Per-user OAuth для MCP-серверов: Keycloak + Telegram Bot — архитектура Auth Proxy | AiManual
AiManual Logo Ai / Manual.
30 Апр 2026 Гайд

Как реализовать per-user OAuth для MCP-серверов с Keycloak и Telegram-ботом: архитектура Auth Proxy

Гайд по внедрению per-user OAuth для мультитенантных AI-агентов. Используем Keycloak, Telegram-бот и Auth Proxy для безопасного доступа к MCP-серверам.

Ваш MCP-сервер не знает, кто вы. И это проблема

Представьте: вы развернули MCP-сервер для работы с корпоративной CRM. Он умеет создавать сделки, тащить контакты и обновлять статусы. Запускаете на нём MCP Orchestrator — и тут выясняется, что каждый AI-агент видит все данные. Без разницы, кто спрашивает: Петя из продаж или Вася из бухгалтерии. MCP-сервер по умолчанию слеп к пользователям. В однопользовательском сценарии это ок, но в мультитенантной среде — выстрел в ногу.

Давайте честно: MCP без аутентификации — это офис с открытыми дверями. Каждый может зайти и взять документы. Но только до первого аудита. Недавнее исследование 2181 MCP-эндпоинтов показало, что 37% из них требуют аутентификации, но лишь единицы делают это per-user. Остальные тупо проверяют статический API-ключ. Это не про безопасность.

Два пути, и один ведёт к Tech Debt

Вариант А: встроить OAuth-логику в каждый MCP-сервер. Прописать Keycloak client, обрабатывать редиректы, хранить токены. Звучит логично, но есть нюанс:

  • Меняется протокол — каждый сервер придётся переписывать.
  • Усложняется конфигурация — если серверов 10, то OAuth нужно настраивать в каждом.
  • Смешивается логика авторизации и бизнес-логики — плохая архитектура в долгосрочной перспективе.

Вариант Б: вынести OAuth в отдельный Auth Proxy. Прокси сидит перед MCP-серверами, проверяет JWT, подставляет заголовок с user_id. MCP-сервер просто читает этот заголовок и работает в контексте пользователя. Никакой магии, только правильный паттерн Sidecar / Gateway. Выбираем Б.

Архитектура Auth Proxy: три кита

Всё строится вокруг трёх компонентов:

КомпонентРоль
Keycloak (IdP)Выпускает JWT, управляет пользователями, client_id/client_secret
Telegram-ботИнициирует OAuth-флоу от имени пользователя, получает токен и отдаёт его клиенту
Auth ProxyПроверяет JWT, пробрасывает заголовок X-User-ID и X-User-Roles на MCP-сервер

Важно: Telegram-бот не хранит токены постоянно. Он получает их, отдаёт клиенту (например, Claude Code или вашему кастомному агенту) и забывает. Клиент кеширует токен локально. Это снижает риск компрометации.

1Разворачиваем Keycloak

Берём последнюю версию Keycloak (на апрель 2026 — это v26.x с поддержкой OAuth 2.1 и PKCE). Поднимаем в Docker:

docker run -d --name keycloak \
  -p 8443:8443 \
  -e KC_HOSTNAME=auth.example.com \
  -e KC_HOSTNAME_STRICT=false \
  -e KC_HTTPS_CERTIFICATE_FILE=/certs/tls.crt \
  -e KC_HTTPS_CERTIFICATE_KEY_FILE=/certs/tls.key \
  quay.io/keycloak/keycloak:26.0.0 start --optimized

Создаём realm mcp-realm, клиент telegram-bot с типом confidential, включаем Standard Flow и PKCE. Для production не забудьте настроить HTTPS (самоподписанный для теста, Let's Encrypt — для прода).

Частая ошибка: не ставят Valid Redirect URIs. Укажите https://t.me/your_bot или, если используете WebApp, точный URL. Иначе Keycloak не примет callback.

2Создаём Telegram-бота для OAuth-флоу

Telegram Bot API сам по себе не поддерживает OAuth-редиректы. Решение: бот генерирует ссылку на Keycloak, пользователь открывает её в браузере, а код авторизации пользователь отправляет обратно боту (или через WebApp). Покажу на Pyrogram — но aiogram тоже подойдёт. Код бота (минимум):

import requests
from pyrogram import Client, filters
import secrets

CLIENT_ID = "telegram-bot"
CLIENT_SECRET = "supersecret"
KEYCLOAK_URL = "https://auth.example.com/realms/mcp-realm/protocol/openid-connect"

app = Client("mcp_oauth_bot")

@app.on_message(filters.command("login"))
async def login(client, message):
    state = secrets.token_urlsafe(16)
    auth_url = f"{KEYCLOAK_URL}/auth?response_type=code&client_id={CLIENT_ID}&state={state}&redirect_uri=myapp://callback"
    await message.reply(f"Перейдите по ссылке для входа: {auth_url}")

@app.on_message(filters.text & ~filters.command)
async def handle_code(client, message):
    code = message.text.strip()
    # обмен authorization code на токен
    resp = requests.post(f"{KEYCLOAK_URL}/token", data={
        "grant_type": "authorization_code",
        "code": code,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": "myapp://callback"
    })
    if resp.ok:
        token = resp.json()["access_token"]
        await message.reply(f"Ваш токен: {token}\nОтправьте его в настройки AI-агента.")
    else:
        await message.reply("Ошибка при получении токена.")

app.run()

В реальном проекте лучше реализовать WebApp: пользователь нажимает кнопку, открывается WebView с Keycloak login, токен приходит через Telegram WebApp API. Но для MVP достаточно ручного копирования.

3Пишем Auth Proxy (сверяем JWT)

Прокси — самое мяско. Можно взять готовый oauth2-proxy или написать свой на Python с aiohttp. Я покажу свой, потому что хочу полный контроль. Плюс — интеграция с любимыми инструментами вроде MCPHero.

import jwt
from aiohttp import web

KEYCLOAK_PUBLIC_KEY = open("keycloak.pem").read()

async def handle(request):
    auth_header = request.headers.get("Authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        return web.json_response({"error": "Unauthorized"}, status=401)
    token = auth_header.split()[-1]
    try:
        payload = jwt.decode(token, KEYCLOAK_PUBLIC_KEY, algorithms=["RS256"],
                             audience="account")
    except jwt.PyJWTError as e:
        return web.json_response({"error": str(e)}, status=401)
    # пробрасываем пользовательские данные на MCP-сервер
    headers = {
        "X-User-ID": payload["sub"],
        "X-User-Roles": ",".join(payload.get("realm_access", {}).get("roles", [])),
        "X-User-Email": payload.get("email", "")
    }
    # здесь ваш клиент MCP-сервера (например, httpx)
    async with web.ClientSession() as session:
        async with session.get("http://mcp-server:8000/mcp", headers=headers) as resp:
            data = await resp.json()
            return web.json_response(data)

app = web.Application()
app.router.add_route('*', '/mcp/{path:.*}', handle)
web.run_app(app, port=8080)

Не забудьте настроить CORS, если прокси вызывается из браузера. И используйте PKCE для защиты от перехвата authorization code.

4Учим MCP-сервер читать заголовки

Ваш MCP-сервер (Python, Node, Go) должен брать X-User-ID и фильтровать данные. Например, в FastMCP:

from fastmcp import FastMCP, Context

mcp = FastMCP("crm")

@mcp.tool()
def get_deals(ctx: Context):
    user_id = ctx.request.headers.get("X-User-ID")
    if not user_id:
        raise Exception("Missing user context")
    # вытащить сделки только для этого пользователя
    return db.query("SELECT * FROM deals WHERE owner_id = ?", user_id)

Теперь агенты, работающие через Claude Code или MCP Chat Studio, будут получать данные строго в контексте пользователя.

Грабли, на которые я наступал (вы — не наступайте)

  • Не использовать HTTPS. JWT — не секрет, перехватили — всё, вы в пролёте. Только HTTPS, даже для localhost (самоподписанный сертификат).
  • Не валидировать issuer и audience. Если не проверить iss и aud, злоумышленник может подставить JWT от другого Keycloak. Всегда проверяйте.
  • Игнорировать refresh token. Access token живёт 5-15 минут. Если не сделать refresh, пользователь будет часто перелогиниваться. Реализуйте механизм: клиент хранит refresh token, при 401 пытается обновить через прокси или напрямую в Keycloak.
  • Хранить client_secret в коде бота. Используйте переменные окружения или Vault. Как настроить groupPolicy и защититься от промпт-инъекций — тема смежная, советую почитать.

Неочевидный совет: храните токены в Vault, а отдавайте через подписанные short-lived сессии

Бот получает токен — это точка уязвимости. Если кто-то взломает бота, он получит все токены пользователей. Лучше: бот не хранит токен вообще. Он создаёт в HashiCorp Vault временную запись (скажем, на 1 час), а клиент по ID сессии получает доступ. Прокси сверяет сессию с Vault и выдёт JWT. Это усложняет архитектуру, но в enterprise без этого не обойтись.

Ещё один лайфхак: используйте Token Exchange в Keycloak. Пользовательский токен (с ограниченными правами) можно обменять на токен для конкретного MCP-сервера с более узкими скоупами. Это реализуется парой строк конфигурации в Keycloak и одним вызовом API через proxy. mcp-context-proxy как раз умеет такое, но уже с другим подходом.

Прогноз: Auth Proxy станет стандартом для корпоративных MCP

Сейчас MCP-экосистема переживает подростковый возраст. Люди запускают серверы без аутентификации, потому что «это же для агентов, а не для людей». Но когда агенты начнут выполнять реальные бизнес-задачи — финансовые операции, изменение документов, — без per-user контроля безопасности не обойтись. Архитектура Auth Proxy с Keycloak и Telegram-ботом — это не единственное решение, но одно из самых гибких. Telegram даёт удобный интерфейс для пользователя, Keycloak — стандарт IdP, а proxy сохраняет MCP-серверы чистыми.

Кстати, в VK-ботах или LM Studio MCP можно применить тот же паттерн — заменить только фронтенд. Удачи.

Подписаться на канал