OAuth

OAuth

Дайте людям входить в ваш клиент через аккаунт Atli и действуйте от их имени по стандартному OAuth 2.1: discovery, PKCE, ротация refresh-токенов и отзыв доступа. Подходит любому клиенту: приложению, боту, сайту, серверу или ИИ-ассистенту.

Когда это нужно

  • ИИ-ассистенты — один из таких клиентов. Подключаясь к MCP-серверу Atli, они сами выполняют вход людей таким способом, так что для них ничего строить не нужно. Один и тот же поток подходит любому клиенту.

  • Любой клиент, действующий от имени человека: мобильное приложение, сайт, бот или сервер, который показывает человеку его eSIM или покупает eSIM для него, с его собственным аккаунтом Atli. Зарегистрируйте свой клиент, и его токены будут работать с API Atli.

  • Люди, вошедшие через принадлежащий вам клиент или по вашему партнёрскому адресу MCP-сервера, могут закрепляться за вами как ваши рефералы. По умолчанию это отключено, и Atli может включить это для вашего аккаунта. В любом случае ваш клиент может сам закрепить за вами приведённых им людей: см. раздел «Закрепите за собой приведённых людей» ниже.

  • Подключиться может только аккаунт, в который можно снова войти где-то ещё: с email, в том числе через Google, Apple или Facebook, либо с Telegram или WhatsApp.

Как это работает

Сервер авторизации — API Atli. Вход и экран согласия находятся на heyatli.com, по адресу /connect, поэтому ваше приложение никогда не видит пароль. Берите все эндпоинты из метаданных, а не прописывайте их вручную. Адреса на этой странице меняются вместе с переключателем Песочница и Рабочая среда.

Зарегистрируйте клиент один раз. Затем для каждого человека: отправьте его на эндпоинт авторизации с PKCE challenge, получите код, с которым он вернётся, обменяйте его на токены и вызывайте API с access-токеном. Обновляйте его, когда он истекает, и отзывайте, когда человек выходит.

curl https://api.heyatli.com/.well-known/oauth-authorization-server

{
  "issuer": "https://api.heyatli.com",
  "authorization_endpoint": "https://api.heyatli.com/oauth2/authorize",
  "token_endpoint": "https://api.heyatli.com/oauth2/token",
  "revocation_endpoint": "https://api.heyatli.com/oauth2/revoke",
  "registration_endpoint": "https://api.heyatli.com/oauth2/register",
  "jwks_uri": "https://api.heyatli.com/oauth2/jwks",
  "scopes_supported": ["api", "offline_access"],
  "code_challenge_methods_supported": ["S256"],
  "client_id_metadata_document_supported": true,
  …
}

Для чего токен

Каждый запрос указывает адрес, для которого предназначен его токен, в resource (RFC 8707), и токен работает только там. Адрес не из этого списка или несуществующий реферальный код отклоняются с invalid_target, так что угадать ни то, ни другое нельзя.

АдресКто может его запроситьЧто делает токен
API Atli
https://api.heyatli.com
Только принадлежащий вам клиент

Bearer-токен для API, действующий от имени человека, как токен из /login.

MCP-сервер Atli
https://mcp.heyatli.com
Любой клиент

Позволяет ассистенту действовать от имени человека через инструменты MCP.

Ваш партнёрский адрес
https://mcp.heyatli.com/r/YOUR_CODE
Любой клиент

То же самое, и человек может закрепиться за вами как ваш реферал, когда это включат для вашего аккаунта.

Регистрация клиента

Есть три способа. Для своего приложения зарегистрируйте собственный клиент: только такой может запрашивать доступ к API Atli.

  1. Собственный клиент

    Войдя в свой аккаунт, вызовите POST /user/oauth-clients. Можно иметь 10 клиентов. Выбирайте public для всего, что работает на чужом устройстве, например мобильного приложения или страницы в браузере пользователя, и confidential для сервера, который может хранить секрет.

    client_secret конфиденциального клиента есть только в этом ответе и больше нигде. Если вы его потеряете, замените его через POST /user/oauth-clients/{id}/rotate-secret. У вашего аккаунта должен быть email, потому что клиенту нужен реферальный код; если у вас его ещё нет, он будет создан.

    curl https://api.heyatli.com/user/oauth-clients \
      -H 'Authorization: Bearer YOUR_TOKEN' \
      -H 'Content-Type: application/json' \
      -d '{ "name": "Travel Bot",
            "redirect_uris": ["https://bot.example/oauth/callback"],
            "client_type": "confidential" }'
    
    {
      "client_id": "atli_client_7f3a9c2e…",
      "client_secret": "…",
      "client_type": "confidential",
      …
    }
  2. Динамическая регистрация

    POST /oauth2/register (RFC 7591), без входа. Так регистрируется большинство ассистентов. Допускается 10 регистраций в час с одного адреса, а клиент, к которому так никто и не подключился, удаляется через 30 дней.

    curl https://api.heyatli.com/oauth2/register \
      -H 'Content-Type: application/json' \
      -d '{ "client_name": "Travel Bot",
            "redirect_uris": ["https://bot.example/oauth/callback"] }'
    
    {
      "client_id": "atli_client_…",
      "token_endpoint_auth_method": "none",
      …
    }
  3. Документ метаданных client ID

    Используйте в качестве client_id размещённый у вас https-адрес, по которому отдаётся JSON-документ с описанием клиента. Atli читает его, когда кто-то входит, так что регистрировать ничего не нужно. Спецификация MCP предпочитает этот способ динамической регистрации.

    // Served at https://bot.example/oauth/client.json
    {
      "client_id": "https://bot.example/oauth/client.json",
      "client_name": "Travel Bot",
      "redirect_uris": ["https://bot.example/oauth/callback"],
      "token_endpoint_auth_method": "none"
    }
  • Redirect URI сравниваются точно. Каждый из них — https или http на localhost, 127.0.0.1 или [::1], где может отличаться только порт, и без фрагмента. У клиента их может быть до 10.

Вход человека

Поток authorization code с PKCE (S256), который используют все клиенты, публичные и конфиденциальные.

  1. Создайте пару PKCE

    Сохраните verifier для обмена кода, а его SHA-256 challenge отправьте с запросом. Для каждого входа создавайте новую пару и новый state.

    const base64url = (bytes) =>
      btoa(String.fromCharCode(...bytes))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    
    const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
    const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
    const challenge = base64url(new Uint8Array(digest));
    const state = base64url(crypto.getRandomValues(new Uint8Array(16)));
  2. Отправьте человека на вход

    Откройте эндпоинт авторизации в его браузере. scope — это api, а offline_access добавляет refresh-токен. Человек входит и даёт согласие на heyatli.com/connect, где видно название вашего клиента, куда он вернёт человека и станет ли тот чьим-то рефералом. Запрос действует 10 минут.

    const url = new URL('https://api.heyatli.com/oauth2/authorize');
    url.search = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: 'https://bot.example/oauth/callback',
      scope: 'api offline_access',
      state,
      code_challenge: challenge,
      code_challenge_method: 'S256',
      // Atli's API, for a client you own; or the MCP server, https://mcp.heyatli.com
      resource: 'https://api.heyatli.com',
    }).toString();
    window.location.assign(url);
  3. Примите код, с которым он вернётся

    Человек возвращается на ваш redirect URI с code, вашим state и iss. Проверьте, что state ваш, а iss — это издатель. Если человек отказал, вместо этого придёт error=access_denied.

    https://bot.example/oauth/callback?code=4bd81f…&state=…&iss=https%3A%2F%2Fapi.heyatli.com
    
    https://bot.example/oauth/callback?error=access_denied&state=…&iss=https%3A%2F%2Fapi.heyatli.com
  4. Обменяйте код на токены

    Отправьте код вместе с verifier, тем же redirect_uri и resource, а также учётными данными клиента, если они есть. Код действует 60 секунд и срабатывает один раз: если отправить его дважды или с неверным verifier, доступ человека прекращается, ведь код мог попасть к кому-то другому.

    Access-токен действует expires_in секунд, то есть 15 минут. Вместе с ним всегда приходит refresh-токен.

    curl https://api.heyatli.com/oauth2/token \
      -u 'CLIENT_ID:CLIENT_SECRET' \
      -d grant_type=authorization_code \
      -d code=THE_CODE \
      -d code_verifier=THE_VERIFIER \
      -d redirect_uri=https://bot.example/oauth/callback \
      -d client_id=CLIENT_ID \
      -d resource=https://api.heyatli.com
    
    {
      "access_token": "…",
      "token_type": "Bearer",
      "expires_in": 900,
      "refresh_token": "…",
      "scope": "api offline_access"
    }

Сохранение входа

Когда access-токен истечёт, запросите новый с помощью refresh-токена. Можно запросить более узкий scope, но никогда не более широкий.

Refresh-токен меняется при каждом использовании, поэтому каждый раз сохраняйте новый. Если ответ до вас не дошёл, повторная отправка старого токена в течение короткого времени вернёт тот же ответ. После этого старый токен означает, что его скопировали, и доступ человека прекращается.

curl https://api.heyatli.com/oauth2/token \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d grant_type=refresh_token \
  -d refresh_token=THE_REFRESH_TOKEN \
  -d client_id=CLIENT_ID \
  -d resource=https://api.heyatli.com

Использование токена

Отправляйте его как Authorization: Bearer ACCESS_TOKEN на адрес, для которого он выдан, и никуда больше.

Токен для API Atli действует от имени человека на всех маршрутах API, как токен из /login. Считайте его непрозрачным: его содержимое может меняться.

Токен для MCP-сервера — это JWT, подписанный EdDSA. Сервер проверяет его по ключам из /oauth2/jwks, по его kid, и проверяет, что его aud — это собственный адрес сервера. В нём нет email и других данных: только sub (id человека), scope, grant_id и время.

curl https://api.heyatli.com/esims \
  -H 'Authorization: Bearer ACCESS_TOKEN'

Закрепите за собой приведённых людей

Закрепление за вами людей, вошедших через ваш клиент или по вашему партнёрскому адресу, по умолчанию отключено, и Atli может включить его для вашего аккаунта. Включено оно или нет, ваш клиент может сам привязать вошедшего человека к вашему реферальному коду с помощью его access-токена: POST /user/referral/redeem с вашим code.

result сообщает, что произошло: ok — человек привязан, already — у него уже есть реферер, notnew — он уже покупал раньше, own — это ваш собственный код, notfound — такого кода нет. Привязать можно только человека без реферера, который ещё ничего не покупал, и только один раз.

curl https://api.heyatli.com/user/referral/redeem \
  -H 'Authorization: Bearer ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "code": "YOUR_CODE" }'

{
  "success": true,
  "result": "ok",
  "message": "…",
  "discount_percent": 10
}

Выход и отзыв доступа

Когда человек выходит из вашего приложения, отзовите его refresh-токен. Это прекращает его доступ в вашем клиенте вместе со всеми токенами. Эндпоинт отвечает 200 даже на незнакомый токен, чтобы с его помощью нельзя было выяснить, какие токены существуют.

Люди также могут сами посмотреть подключённые приложения и отозвать у них доступ (GET /user/oauth-grants, DELETE /user/oauth-grants/{id}). После этого ваш клиент не сможет получить новый токен, поэтому считайте отклонённое обновление выходом.

curl https://api.heyatli.com/oauth2/revoke \
  -u 'CLIENT_ID:CLIENT_SECRET' \
  -d token=THE_REFRESH_TOKEN \
  -d client_id=CLIENT_ID

Ошибки

Эндпоинты токена, отзыва и регистрации отвечают собственным форматом ошибки OAuth, {"error": "invalid_grant", "error_description": "…"}, а не обычным форматом API. Ориентируйтесь на error, а не на описание. Каждый ответ эндпоинта токена содержит Cache-Control: no-store.

  • invalid_grant: код уже использован или истёк, verifier неверный, либо refresh-токен использован повторно после льготного периода. Выполните вход человека заново.

  • invalid_target: resource не является адресом, для которого может быть токен, или указывает несуществующий реферальный код.

  • invalid_client: учётные данные клиента неверны или отсутствуют. Любой тип клиента получает одну и ту же ошибку.

  • invalid_scope: при обновлении запрошено больше, чем разрешил человек.