Skip to content
страница целиком, в Markdown — вставьте в чат с моделью

Подключение сервиса ​

Аудитория: бэкенд- и фронтенд-разработчики сервиса экосистемы. Стек любой — контракт чисто HTTP.

Всё, что ниже, относится к базовому URL платформы вашего окружения. В примерах — https://api.aseller.cat, все пути идут с префиксом /api/v1.

Шаг 0. Сервисный кред ​

Сгенерируйте секрет и посчитайте его sha256 — платформе отдаётся только хеш:

bash
SECRET=$(openssl rand -hex 32)
printf %s "$SECRET" | sha256sum   # 64 hex-символа — это уходит админу платформы
echo "$SECRET"                    # это остаётся у вас, в переменной окружения

Секрет хранится на стороне сервиса (env / Vault / Consul) и подставляется в заголовки серверных вызовов:

http
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>

Секрет — серверный

Сервисный кред никогда не уходит в браузер. Он нужен только POST /sso/token и обогащённому POST /auth/validate. Утечка секрета = возможность обменивать чужие sso-коды на токены.

Шаг 1. Проверка пользовательского токена ​

Это минимум, который нужен даже без своего входа: фронт получил Bearer от Host UI или от вашего SSO — бэкенд обязан проверить его у платформы.

http
POST /api/v1/auth/validate
Authorization: Bearer <пользовательский ACCESS>

Ответ всегда 200, даже для мусорного токена — разбирайте поле valid:

json
{
  "valid": true,
  "user": {
    "id": "6f2d…",
    "role": 10,
    "email": "client@example.com",
    "name": "Иван"
  },
  "epoch": 4,
  "expiresAt": "2026-09-25T12:34:56.000Z"
}

Невалидный токен:

json
{ "valid": false, "reason": "session_revoked" }

Причины отказа ​

reasonЧто случилось
missing_authorizationНет заголовка Authorization
unsupported_authorization_typeНе Bearer или пустой токен
invalid_or_expiredТокен не разбирается, истёк или не прошёл проверку подписи
wrong_token_typeПрислан не ACCESS-токен (например, refresh)
user_pending_confirmationПользователь в роли 50 — регистрация не подтверждена
session_revokedСессии с таким токеном больше нет: logout, «выйти везде», смена пароля
validation_unavailableПлатформа не смогла проверить (Redis недоступен) — это не «токен плохой», повторите позже

Обогащённый ответ ​

С сервисным кредом можно попросить больше — права, магазины, мастера, доступные роли:

http
POST /api/v1/auth/validate?include=permissions,shopIds,masterUser,allowedRoles
Authorization: Bearer <пользовательский ACCESS>
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>
json
{
  "valid": true,
  "user": { "id": "6f2d…", "role": 10, "email": "client@example.com",
            "name": "Иван", "masterUser": null, "allowedRoles": [10, 40] },
  "permissions": ["ACCESS_PRODUCTS", "EXT_IDEA_CALC_TOOLS"],
  "shopIds": ["a1b2…", "c3d4…"],
  "epoch": 4,
  "expiresAt": "2026-09-25T12:34:56.000Z"
}

Без валидного креда include молча игнорируется, а в ответе появляется "includeDenied": "invalid_service_credential" — если вы ждали permissions, а их нет, проверьте это поле первым делом.

Кэширование и epoch ​

Валидация стоит поход в Redis — не зовите её на каждый запрос.

  • Кэшируйте позитивный вердикт на ~30 секунд по ключу sha256(token).
  • Негативный — не дольше 5 секунд, иначе logout будет виден с задержкой.
  • Поле epoch — счётчик инвалидации прав пользователя на платформе. Он растёт, когда права или магазины меняются. Запомните значение рядом с кэшем: выросло — сбросьте свои кэши по этому пользователю, даже если ваш TTL ещё не истёк.

Сама платформа держит внутренний LRU вердиктов на 5 секунд (AUTH_VALIDATE_VERDICT_TTL_MS), поэтому ответы в пределах этого окна приходят мгновенно.

Связывание с вашим аккаунтом ​

В payload всех ACCESS-токенов есть email — он же приходит в user.email. Это человекочитаемый идентификатор, по которому сервис при первом обращении линкует платформенного пользователя со своей записью. user.id (UUID платформы) — то, что нужно хранить как внешний ключ: email может смениться, id — нет.

Шаг 2. Браузерный вход «Войти через Aseller» ​

2.1. Фронт сервиса просит код ​

Страницу платформы открывает фронт платформы — у него уже есть ACCESS-токен пользователя. Он зовёт:

http
POST /api/v1/sso/grant
Authorization: Bearer <ACCESS пользователя>
Content-Type: application/json

{
  "client": "idea_calc",
  "redirect_uri": "https://calc.aseller.cat/sso/callback",
  "state": "<случайная строка ≥8 символов, ваш CSRF-параметр>"
}

Ответ:

json
{ "redirect": "https://calc.aseller.cat/sso/callback#sso_code=sso_code_Xk…&state=…" }

Браузер переходит по redirect. Код лежит во fragment, а не в query: fragment не попадает ни в логи nginx, ни в Referer.

Отказы: 404 Unknown SSO client — сервиса нет в реестре, он неактивен или у него не включён ssoEnabled; 403 redirect_uri is not allowed for this client — адрес не совпал с allowlist. Ограничение — 30 запросов в минуту.

Сравнение redirect_uri — префиксное

В реестре лежат префиксы. https://calc.aseller.cat/sso/callback?next=/reports пройдёт при префиксе https://calc.aseller.cat/sso/callback. Требования к адресу: абсолютный http(s) и без собственного fragment.

2.2. Фронт сервиса отдаёт код своему бэкенду ​

Страница /sso/callback читает location.hash, проверяет state и отправляет sso_code на свой бэкенд. Дальше код в браузере не нужен — уберите его из адресной строки.

2.3. Бэкенд меняет код на токены ​

http
POST /api/v1/sso/token
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>
Content-Type: application/json

{ "grant": "code", "code": "sso_code_Xk…" }
json
{
  "accessToken": "eyJhbGciOi…",
  "accessExpiresAt": "2026-09-25T12:01:00.000Z",
  "refreshToken": "eyJhbGciOi…",
  "user": { "id": "6f2d…", "role": 10, "email": "client@example.com",
            "name": "Иван", "masterUser": null, "allowedRoles": [10] },
  "permissions": ["ACCESS_PRODUCTS"],
  "shopIds": ["a1b2…"],
  "epoch": 4
}

Код одноразовый и живёт 60 секунд (SSO_CODE_TTL_SEC). Повторный обмен вернёт invalid_grant.

2.4. Обновление токенов ​

accessToken живёт 60 секунд — это не опечатка. Он рассчитан на то, что сервис держит у себя долгоживущий refreshToken и обновляет пару в фоне:

http
POST /api/v1/sso/token
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>

{ "grant": "refresh", "refreshToken": "eyJhbGciOi…" }

Ответ той же формы — с новым refresh-токеном.

Ротация и обнаружение повтора

Каждый обмен отзывает предъявленный refresh и выдаёт новый. Предъявили уже использованный — платформа считает это компрометацией и отзывает всю цепочку этого пользователя для вашего сервиса: ответ invalid_grant с Refresh token reuse detected, chain revoked, пользователю придётся войти заново.

Отсюда практическое правило: обмен refresh-токена должен быть атомарным на вашей стороне. Два параллельных запроса с одним и тем же refresh — это самострел. Сериализуйте обновление (мьютекс на пользователя) и сохраняйте новый токен до того, как отдадите ответ своим клиентам.

Ошибки /sso/token:

КодerrorКогда
401invalid_clientНеизвестный X-Service-Client или неверный секрет
403unauthorized_clientУ сервиса в реестре ssoEnabled = false
400invalid_requestНе передан refreshToken при grant=refresh
400invalid_grantКод истёк / уже использован / выдан другому клиенту; refresh неизвестен, истёк, чужой аудитории или повторно использован
403access_deniedПользователь деактивирован или в роли 50

Ограничение — 60 запросов в минуту на сервис.

2.5. Что делать с полученным ACCESS ​

Он ведёт себя как обычный пользовательский токен платформы: с ним можно ходить в API Aseller от имени пользователя и его же проверять через /auth/validate. Внутри сервиса удобнее опираться на свою сессию, а платформенный ACCESS держать для исходящих вызовов в Aseller.

Шаг 3. Реакция на выход и смену пароля ​

Событие на платформеЧто происходит с вашими токенами
Обычный logout пользователяСессия ACCESS гаснет → /auth/validate отвечает session_revoked. Ваш refreshToken продолжает работать
«Выйти везде» / смена пароля / восстановление пароляОтзываются все SERVICE_REFRESH пользователя — ваш refresh перестаёт обмениваться, нужен новый вход
Деактивация пользователя/sso/token отвечает access_denied, /auth/validate — valid: false

Отсюда: не считайте живой refresh доказательством, что пользователь всё ещё «внутри» — состояние сессии показывает только /auth/validate.

Чек-лист интеграции ​

  • [ ] Секрет сгенерирован, хеш передан админу, сам секрет лежит в env сервиса
  • [ ] Бэкенд проверяет каждый пользовательский токен через /auth/validate и кэширует вердикт (30 с позитив / 5 с негатив)
  • [ ] epoch сохраняется вместе с кэшем и сбрасывает его при росте
  • [ ] Платформенный user.id хранится как внешний ключ, email — только для первичной линковки
  • [ ] state генерируется на каждый вход и проверяется в колбэке
  • [ ] sso_code уходит с фронта на бэкенд и вычищается из адресной строки
  • [ ] Обновление refresh сериализовано, новый токен сохраняется до ответа клиенту
  • [ ] Ответ validation_unavailable обрабатывается как временная ошибка, а не как отказ в доступе

Документация по инфраструктуре