Подключение сервиса
Аудитория: бэкенд- и фронтенд-разработчики сервиса экосистемы. Стек любой — контракт чисто HTTP.
Всё, что ниже, относится к базовому URL платформы вашего окружения. В примерах — https://api.aseller.cat, все пути идут с префиксом /api/v1.
Шаг 0. Сервисный кред
Сгенерируйте секрет и посчитайте его sha256 — платформе отдаётся только хеш:
SECRET=$(openssl rand -hex 32)
printf %s "$SECRET" | sha256sum # 64 hex-символа — это уходит админу платформы
echo "$SECRET" # это остаётся у вас, в переменной окруженияСекрет хранится на стороне сервиса (env / Vault / Consul) и подставляется в заголовки серверных вызовов:
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>Секрет — серверный
Сервисный кред никогда не уходит в браузер. Он нужен только POST /sso/token и обогащённому POST /auth/validate. Утечка секрета = возможность обменивать чужие sso-коды на токены.
Шаг 1. Проверка пользовательского токена
Это минимум, который нужен даже без своего входа: фронт получил Bearer от Host UI или от вашего SSO — бэкенд обязан проверить его у платформы.
POST /api/v1/auth/validate
Authorization: Bearer <пользовательский ACCESS>Ответ всегда 200, даже для мусорного токена — разбирайте поле valid:
{
"valid": true,
"user": {
"id": "6f2d…",
"role": 10,
"email": "client@example.com",
"name": "Иван"
},
"epoch": 4,
"expiresAt": "2026-09-25T12:34:56.000Z"
}Невалидный токен:
{ "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 недоступен) — это не «токен плохой», повторите позже |
Обогащённый ответ
С сервисным кредом можно попросить больше — права, магазины, мастера, доступные роли:
POST /api/v1/auth/validate?include=permissions,shopIds,masterUser,allowedRoles
Authorization: Bearer <пользовательский ACCESS>
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>{
"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-токен пользователя. Он зовёт:
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-параметр>"
}Ответ:
{ "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. Бэкенд меняет код на токены
POST /api/v1/sso/token
X-Service-Client: idea_calc
X-Service-Secret: <SECRET>
Content-Type: application/json
{ "grant": "code", "code": "sso_code_Xk…" }{
"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 и обновляет пару в фоне:
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 | Когда |
|---|---|---|
401 | invalid_client | Неизвестный X-Service-Client или неверный секрет |
403 | unauthorized_client | У сервиса в реестре ssoEnabled = false |
400 | invalid_request | Не передан refreshToken при grant=refresh |
400 | invalid_grant | Код истёк / уже использован / выдан другому клиенту; refresh неизвестен, истёк, чужой аудитории или повторно использован |
403 | access_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обрабатывается как временная ошибка, а не как отказ в доступе