Головна / Статті

Двосторонній кордон OAuth для шлюзів MCP: збереження ідентичності користувача

Шлюзи MCP не повинні об’єднувати кожен виклик у один обліковий запис сервісу. Вхідна перевірка разом із обміном даними від імені OAuth зберігає цілісність ідентичності кожного користувача.

Видавець: REACTAPP.TOP11 хв читання2444 слів

Джерела: Solving the Identity Termination Problem in MCP Gateway Architectures

Шлюз, який знаходиться між агентом, керованим MCP, та корпоративними системами, повинен мати два кордони OAuth, щоб особа, яка подала запит, залишалася ідентифікованою в кожній системі, до якої торкається агент.

Порушення

Розглянемо типове порушення. Співробітник через шлюз MCP просить агента оформити заявку для колеги. Шлюз перевіряє співробітника в корпоративному постачальнику ідентифікації, підтверджує токен-носій та передає запит до API ITSM, який створює заявку. Через місяць аудитор відкриває базу заявок та бачить одне ім’я у всіх заявках, оформлених агентом: це ідентичність власного додатку шлюза. Справжні заявники відсутні в записах.

Та сама проблема повторюється у кожній системі, що знаходиться після шлюза. Система кадрів приписує кожну заявку на відпустку до облікового запису сервісу шлюза, реєстр активів — кожну передачу пристрою йому, а процес закупівель вказує його як особу, яка схвалює кожну заявку. Ідентичність закінчується біля шлюза, і жодна наступна система не може її відновити.

Шлюзи MCP стали стандартним містком між агентами LLM та корпоративними системами зберігання даних, проте більшість продакшн-реалізацій залишають одне питання невизначеним: чия ідентичність авторизує кожен виклик до наступних систем? Домінують два антипатерни.

Закінчена ідентичність. Після перевірки того, хто телефонує, шлюз взаємодіє з кожним бекендом як з єдиним спільним обліковим записом сервісу. Усе, що він робить з ІТ-системою, системою кадрів чи системою закупівель, приписується його обліковому запису сервісу, а не особі, яка подала запит. Журнали аудиту стають марними, а принцип мінімальних привілеїв порушується. Неналежно працюючий запит може надати шлюзу повні повноваження щодо будь-якого бекенду, до якого він може отримати доступ.

Довіряйте клієнту. Шлюз приймає будь-який токен-носій, який надходить, та передає його без змін до API нижчого рівня, не перевіряючи емітента, цільову аудиторію чи обсяг повноважень. Крадіжка токена на будь-якому етапі передачі потім мовчки авторизує довільні запити до систем нижчого рівня.

Жоден з варіантів не є прийнятним для корпоративних агентних ШІ, особливо згідно з функцією управління ризиками ШІ NIST AI Risk Management Framework, яка вимагає збереження індивідуальної відповідальності під час автоматизованих дій у регульованих системах.

Пропозиція

Дизайн має два межі OAuth. На кінцевому пристрої клієнта вхідний меж вилучає та перевіряє токен делегованого доступу, тобто токен типу bearer OAuth 2.0 (RFC 6749), виданий користувачеві, який здійснює запит, для аудиторії шлюзу. Вихідний меж здійснює обмін токенами від імені шлюзу (згідно з принципами RFC 8693) або, якщо це дозволяє ланцюг довіри, здійснює пряму передачу делегованих токенів, створюючи токен для наступної аудиторії, який все ще містить ідентифікатор оригінального користувача.

Дозвіл на використання інструменту визначається на межі, на основі оброблених токенів-тверджень, а не всередині реалізації інструменту. Це робить передачу ідентичності завданням шлюзу, а не чимось, що кожен інструмент чи бекенд повинні пам’ятати.

Паттерн: Delegated Boundary OAuth

Коротко кажучи: шлюз використовує дві межі OAuth, а не одну, і ідентичність користувача є контрактом, який передається між ними.

Межа вхідних запитів. Вилучення токена-носія, перевірка емітента та аудиторії, а також аналіз тверджень відбуваються у одному шарі шлюза — зазвичай у проміжному програмному забезпеченні запитів або у інжекторі залежностей у стилі FastMCP — який виконується перед кожною функцією інструменту. Саме тут відповідається на запитання „Хто телефонує та чи дозволено йому використовувати цей інструмент?“, і жодна реалізація інструменту не може його проігнорувати. Пов’язування діапазонів дозволів із інструментами здійснюється декларативно: кожен доступний інструмент вказує діапазони OAuth, які йому потрібні, і межа відмовляється його запустити, якщо токен їх не містить. Ця відмова є структурованою помилкою авторизації, а не помилкою 5xx від перевірки дозволів, яка спричинила зайві витрати ресурсів під час виконання.

Межа вихідного напрямку. Дзвінок у напрямку нижчого рівня автентифікується за допомогою токена, який вказує ім’я початкового користувача, а не ідентичності самого шлюзу. Цього досягають двома механізмами. Механізм, що діє від імені обмінника (Microsoft Entra OBO, обмінник ідентичностей AWS Cognito або власний обмінник STS), видає новий токен, адресований API нижчого рівня, причому суб’єктом залишається людина, яка здійснила дзвінок. Пряме пересилання, призначене для сервісів нижчого рівня, які підпадають під чітку угоду про довіру з шлюзом, передає вхідний токен без змін. Саме обмінник забезпечує можливість підтвердження передачі токена, а не лише її очікування.

Між проханнями. Токени розглядаються як стан, який існує протягом одного запиту. Нічого не зберігається у кеші між запитами, нічого не зберігається у сесії, і жоден змінний стан не ділиться між процесами. Кожен виклик інструменту MCP знову читає токен з поточних заголовків та забуває про нього після завершення запиту.

Реалізація

Надходження даних: отримання делегованого токена

Межа надходження даних — це простий мідлвейр, який виконується перед кожною функцією інструменту. У Python MCP сервері, побудованому на фреймворку типу FastMCP, ви користуєтесь параметром CurrentHeaders та отримуєте токен-носій за допомогою невеликої утиліти:

from typing import Optional

def extract_bearer_token(headers: dict) -> str:
    auth = headers.get("authorization") or headers.get("Authorization")
    if not auth or not auth.startswith("Bearer "):
        raise AuthError("missing or malformed bearer token")
    return auth[len("Bearer "):]

Далі відбувається перевірка цього токена щодо емітента та аудиторії, яких очікує шлюз, за допомогою оприлюднених ключів підпису постачальника ідентичності (JWKS):

from jose import jwt

def validate_inbound_token(token: str, jwks: dict, audience: str, issuer: str) -> dict:
    claims = jwt.decode(
        token, jwks,
        audience=audience,
        issuer=issuer,
        options={"require_exp": True, "require_iat": True},
    )
    return claims  # sub, scp/scope, exp, etc.

Перевірка має відбуватися безпосередньо в процесі. Передача обов’язків з перевірки JWT на проксі нижчого рівня може спрацювати в структурах довіри з обмеженими можливостями, але це позбавляє шлюз можливості забезпечити відповідність між діапазоном дозволів та інструментом ще до того, як інструмент почне виконувати свої дії.

Вихідний процес: дії від імені обмінника

На вихідному рівні користувацький токен, отриманий вхідним шляхом, замінюється токеном, призначеним для аудиторії нижчого рівня. Саме ця заміна відрізняє цей підхід від простого пересилання, оскільки API нижчого рівня ніколи не бачить токен вхідного шляху безпосередньо. Шлюз викликає кінцеву точку токенів постачальника ідентичності з запитом типу urn:ietf:params:oauth:grant-type:jwt-bearer (Entra) або запитом assume-role-with-web-identity (AWS), передаючи токен вхідного шляху як дані суб’єкта. Постачальник повертає новий токен для API нижчого рівня, у якого значення поля sub залишається таким самим, як у початкового користувача.

def exchange_obo(inbound_token: str, downstream_audience: str) -> str:
    response = requests.post(
        f"{IDP_TOKEN_ENDPOINT}",
        data={
            "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
            "client_id": GATEWAY_CLIENT_ID,
            "client_secret": GATEWAY_CLIENT_SECRET,
            "assertion": inbound_token,
            "scope": f"{downstream_audience}/.default",
            "requested_token_use": "on_behalf_of",
        },
        timeout=5,
    )
    response.raise_for_status()
    return response.json()["access_token"]

Отриманий токен потім додається до вихідного HTTP-запиту:

def call_downstream(downstream_token: str, path: str) -> dict:
    return requests.get(
        f"{DOWNSTREAM_BASE_URL}{path}",
        headers={"Authorization": f"Bearer {downstream_token}"},
        timeout=10,
    ).json()

Картування діапазонів та поширення ідентичності

Функції інструменту оголошують свої необхідні діапазони під час реєстрації, а механізм контролю забезпечує їх дотримання перед виконанням основної частини інструменту:

@tool(required_scopes=["ticketing.write"])
def open_ticket(headers: CurrentHeaders, summary: str, body: str) -> dict:
    token = extract_bearer_token(headers)
    claims = validate_inbound_token(token, jwks, audience=GATEWAY_AUD, issuer=ISSUER)
    enforce_scopes(claims, required=["ticketing.write"])  # central, not tool-local
    downstream_token = exchange_obo(token, downstream_audience=TICKETING_AUD)
    return call_downstream(downstream_token, path="/tickets")

Оскільки мапування діапазонів є декларативним, його можна перевірити. Будь-хто, хто аудитує шлюз, отримує всі інструменти та необхідні для них діапазони безпосередньо з реєстру, не потрібно читати код інструментів.

Вхідні межі є однаковими у всіх постачальників хмарних послуг. Відрізняється лише спосіб з’єднання для вихідних даних, і обидві наведені нижче реалізації мають однакову загальну структуру.

У AWS: API Gateway, Lambda та Cognito

У AWS вхідний бар’єр може бути спеціальним авторизатором Lambda у Amazon API Gateway. Він перевіряє токен проти пулу користувачів Amazon Cognito та повертає документ політики разом із картою контексту, що містить розібрані твердження, тож сервер MCP Lambda отримує ці твердження з event.requestContext.authorizer без необхідності їх повторної перевірки. Щодо вихідного боку, операція sts:AssumeRoleWithWebIdentity бере вхідний токен (або токен, виданий пулом ідентичностей Cognito) та створює тимчасові облікові дані AWS, прив’язані до федерованої ідентичності користувача. Будь-який кінцевий пункт API Gateway чи зовнішня API, яка довіряє JWT від Cognito, потім бачить користувача у твердженні sub.

Мінімальна заміна даних між Cognito та STS у Lambda gateway виглядає так:

import boto3

def aws_obo(inbound_token: str, role_arn: str) -> dict:
    sts = boto3.client("sts")
    creds = sts.assume_role_with_web_identity(
        RoleArn=role_arn,
        RoleSessionName="mcp-gateway-obo",
        WebIdentityToken=inbound_token,
        DurationSeconds=900,
    )
    return creds["Credentials"]

Політика довіри призначеної ролі вказує пул користувачів Cognito як федерованого суб’єкта та також перевіряє значення aud у надісланому токені, що означає, що лише токен, створений для цього шлюзу, може виконувати цю роль.

У Azure: процес Entra on-behalf-of

У Azure межовий контроль надходження перевіряє надісланий токен за реєстрацією додатку Entra шлюзу, використовуючи URI Application ID шлюзу як аудиторію. Для вихідного кроку відбувається обмін даними за процесом Entra on-behalf-of, спрямований на власну реєстрацію додатку нижчезнаходженої API:

import msal

def entra_obo(inbound_token: str, downstream_scope: str) -> str:
    app = msal.ConfidentialClientApplication(
        client_id=GATEWAY_APP_ID,
        client_credential=GATEWAY_CLIENT_SECRET,
        authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    )
    result = app.acquire_token_on_behalf_of(
        user_assertion=inbound_token,
        scopes=[downstream_scope],
    )
    if "access_token" not in result:
        raise AuthError(result.get("error_description", "obo exchange failed"))
    return result["access_token"]

API нижчого рівня має приймати токени, які призначені для реєстрації його власного додатку Entra. Його логіка авторизації читає значення oid (ID об’єкта) або upn (ім’я основного користувача), щоб прив’язати виклик до початкового користувача. Структура шаблону залишається ідентичною, змінюється лише спосіб підключення постачальника ідентичності.

Пов’язування з журналуванням та оцінкою

Ідентичність, яка поширюється, найбільш корисна, коли вона фіксується. Якщо шлюз записує інформацію про користувача під час кожного виклику інструменту, аудит дозволяє прив’язати дію агента до конкретної особи. Те саме поле може бути включене до записів оцінки, що дозволяє пов’язати тести з особами, чия поведінка є об’єктом реакції системи. Вважайте ідентичність, яку отримує цей шаблон, першим стовпцем кожного запису аудиту та оцінки, який створює система.

Коли цей шаблон підходить, а коли — ні

Він підходить, коли:

  • Шлюз обробляє системи, де індивідуальна ідентичність має значення для авторизації, аудиту чи дотримання правил, такі як управління ІТ-послугами, кадровий відділ, фінанси та регульовані робочі процеси.
  • Постачальник корпоративних ідентифікаторів підтримує стандарти обміну ідентифікаторами: Entra OBO, Cognito з веб-ідентифікацією STS чи обмін токенами Auth0 та Okta.
  • API, що знаходяться нижче в ланцюжку, розташовані в межах того ж простору довіри, що й шлюз, і можуть бути налаштовані для прийому токенів користувачів.
  • Реалізація підпадає під регулювання NIST AI RMF, SOX, HIPAA чи подібні стандарти, які вимагають відповідальності на рівні користувача за автоматизовані дії.

Це не підходить у таких випадках:

  • Кожен виклик, що знаходиться нижче в ланцюжку, явно не є специфічним для користувача, наприклад, це лише дані для ознайомлення без будь-яких наслідків, пов’язаних із конкретним користувачем.
  • Надійний проксі на нижчому рівні вже забезпечує повну деlegeовану семантику для шлюзу, що робить роботу самого шлюзу зайвою. У цьому випадку шлюз створює лише вхідну межу та варіант деlegeованої довіри для вихідної сторони, пересилаючи дані безпосередньо відповідно до чіткого договору про довіру та визнаючи, що проксі є справжньою безпековою межею.
  • API на нижчому рівні відхиляє токени користувачів, і їхнє перероблення не є можливим. Відкладіть цю схему до змін у умовах роботи API. Використання сервісного принципала „наразі“ має бути свідомим рішенням із описом пов’язаних ризиків, а не стандартною практикою.
  • Операційні аспекти:

    • Затримка. Кожна операція обміну через OBO коштує додаткової візити до кінцевої точки токенів для кожного вхідного запиту. Зберігайте обмінені токени стільки, скільки дійсний вхідний токен, ідентифікований за ключем (sub, downstream_audience), і ніколи не передавайте їх між користувачами.
    • Обмеження від постачальника ідентичності. Квоти кінцевої точки токенів — це не проста справа. Забезпечте запас місткості та відстежуйте рівень помилок під час обміну.
    • Оновлення. Довгі сеанси агентів тривають довше, ніж термін дії одного токена. Плануйте ротацію токенів оновлення або погоджуйтесь на те, що цикл агента закінчується, коли його токен закінчується, що зазвичай є правильною практикою з точки зору безпеки.
    • Альтернативний сценарій з обліковими записами сервісів. Зберігайте шлях обміну через облікові записи сервісів для справ, які справді не потребують користувачів, таких як перевірки стану чи заплановане фонове синхронізування. Робіть це чітко визначеним, з вузьким діапазоном дозволів та з обов’язковим логуванням.

    Де ця модель має обмеження

    Вона передбачає, що постачальник ідентичностей підтримує стандарт обміну даними від імені іншої сторони, проте багато старіших версій цього не роблять. У таких ситуаціях система змушена передавати токени безпосередньо, за умови наявності письмової угоди про довіру з кожним наступним сервісом. Це прийнятно для бекендів, які знаходяться в межах довірчої зони шлюзу, але недостатньо, коли наступний сервіс працює іншою командою чи стороннім постачальником. Правильним шляхом міграції є оновлення постачальника ідентичностей, а не постійне використання механізму пересилання даних.

    Він також передбачає, що послуги нижчого рівня приймають токени користувача-аудиторії. Багато корпоративних систем існують ще до появи делегованої автентифікації та автентифікуються лише за допомогою принципалів послуг. У такому випадку цю схему доводиться чекати, поки послуга не буде оновлена або попереду не буде встановлений проксі, який розуміє ідентичності. Часткова реалізація — коли деякі виклики автентифікуються за допомогою атрибутів користувача, а інші — атрибутів послуги — є звичайним станом під час міграції, але її слід вважати технічним боргом із визначеною датою усунення, а не нормалізованою схемою.

    Нарешті, це не вирішує проблеми відповідальності агента як виконавця дій. Збереження ідентичності людини під час переходу через шлюз є правильним основним механізмом контролю, проте агент на основі LLM все одно може бути спрямований на виконання дій, яких користувач буквально ніколи не просив. Цей паттерн гарантує приписування дій, але не наміру. Компенсаційні механізми контролю несуть цей тягар: підтвердження людиною для дій із високим впливом, мінімальні обсяги дій на вхідному рівні та облік доказів для кожного виклику.

    Чек-лист для впровадження

    • Документуйте вхідний контракт (видавець, аудиторія, підтримувані обсяги) як версійований інтерфейс та керуйте ним як публічним API.
    • Реалізуйте відповідність обсягів дій інструментам у вигляді декларативного поля під час реєстрації інструментів, а не у вигляді спорадичних перевірок, розкиданих по коді інструментів.
  • Встановіть механізм обміну від імені користувача з постачальником ідентичностей підприємства для кожного заднього сервера. Встановіть OBO як стандартний варіант та збережіть пряму передачу для серверів, що знаходяться в межах чітко визначеної зони довіри шлюза.
  • Зберігайте обмінені токени протягом усього терміну дії вхідного токена, ключуючи їх за ідентифікатором користувача та цільовою аудиторією, та ніколи не діліться ними між користувачами.
  • Під час кожного виклику інструменту фіксуйте як вхідний, так і вихідний значення sub, щоб можна було перевірити весь процес поширення токенів від початку до кінця.
  • Дозвольте використання одного обмеженого за сферою дії облікового запису сервісу для роботи без користувача та вимагайте схвалення щоразу, коли він використовується.
  • Зареєструйте API-сервіси, які відхиляють токени користувачів-аудиторій, як архітектурний дефіцит, та працюйте над усуненням цієї обмеженості.
  • Оскільки американські компанії та федеральні програми впроваджують Рамку управління ризиками ШІ NIST, її функція Govern вимагає наявності індивідуальної відповідальності під час автоматизованих дій у регульованих системах. Відмова від ідентифікації користувачів на рівні входу безпосередньо суперечить цьому та може призвести до виявлення проблем під час аудиту в середовищах SOX, HIPAA та FedRAMP, де кожна автоматизована зміна має бути прив’язана до конкретної особи.

    Додаткова література

    Основою є документи RFC 6749 (Рамка авторизації OAuth 2.0) та RFC 8693 (Обмін токенами OAuth 2.0) від IETF. У документації виробників описано механізм авторизації OAuth 2.0 On-Behalf-Of у платформі ідентифікації Microsoft, API AWS STS AssumeRoleWithWebIdentity та пули ідентифікації Amazon Cognito. NIST публікує Рамку управління ризиками ШІ (AI RMF 1.0), а специфікація MCP містить розділ про авторизацію, який варто прочитати разом із усім вищезазначеним.

    Пов’язана література