Вход через Syndicate
У людей уже есть аккаунт Синдиката для Resonance, ASAI, Apeiron и остальной платформы. Ваше приложение может пустить их по нему, без нового пароля. Схема — OAuth 2.1, код авторизации с PKCE.
Заведите клиент
В Dev Studio откройте Sign-in apps и создайте приложение:
- Название — его человек увидит в окне согласия.
- Redirect URI — до 10, сверяются точно. Нативному приложению подойдёт loopback-адрес (
http://127.0.0.1/callback): порт не сверяется, свободный можно выбрать при запуске. - Scopes — что приложение вообще может запросить. Для
emailиsubscriptionнужно обоснование в заявке разработчика.
client_id (asc_…) выдаётся сразу. Секрет клиента показывается один раз, но эндпоинт токенов его не использует: подлинность клиента держится только на PKCE. Считайте любой клиент публичным и никогда не кладите секрет в браузерный бандл или сборку приложения. На одном аккаунте — до 10 активных клиентов.
Эндпоинты
| Метод | Эндпоинт | Зачем |
|---|---|---|
GET | https://auth.aperturesyndicate.com/api/v1/sso/authorize | Начать вход в браузере |
POST | https://auth.aperturesyndicate.com/api/v1/sso/token | Обменять код, обновить токены |
GET | https://auth.aperturesyndicate.com/api/v1/sso/userinfo | Кто вошёл, с учётом scopes |
GET | https://auth.aperturesyndicate.com/.well-known/jwks.json | Открытые ключи для проверки токенов |
GET | https://auth.aperturesyndicate.com/.well-known/oauth-authorization-server | Метаданные RFC 8414 |
В метаданных перечислены и scopes для ИИ-агентов, которые подключаются к нашему MCP-серверу. Клиент из Dev Studio может запросить только четыре scope ниже.
Scopes
| Scope | Что добавляет в /userinfo |
|---|---|
openid | sub — постоянный id аккаунта |
profile | nickname, avatar_url, profile_url |
email | email, email_verified |
subscription | subscription — действующий тариф и возможности, или null |
Внутреннее имя пользователя не раскрывается нигде — ни в /userinfo, ни в токене. Связывайте аккаунты по sub.
1. Отправьте человека на вход
Сгенерируйте случайный code_verifier (43–128 символов) и code_challenge = base64url(SHA-256(verifier)). Затем перенаправьте:
https://auth.aperturesyndicate.com/api/v1/sso/authorize
?response_type=code
&client_id=asc_…
&redirect_uri=https://yourapp.example/callback
&scope=openid profile
&state=<случайная строка>
&code_challenge=<challenge>
&code_challenge_method=S256| Параметр | |
|---|---|
response_type | Всегда code |
client_id | Из Dev Studio |
redirect_uri | Один из заведённых |
scope | Через пробел, в пределах scopes клиента |
state | Обязателен. Сверьте его на обратном пути |
code_challenge_method | Только S256; plain отклоняется |
prompt | Необязателен. none не показывает никаких экранов и возвращает error=login_required, если человек не вошёл |
screen | Необязателен. register ведёт невошедшего человека на создание аккаунта, а не на вход |
Невошедший человек сначала входит. Потом он всегда видит окно согласия с названием вашего приложения и scopes: стороннее приложение никогда не получает код молча. Если человек согласился, браузер вернётся на:
https://yourapp.example/callback?code=…&state=…Код живёт 10 минут и срабатывает один раз. При отказе или сбое в адресе будет error — с prompt=none это login_required или consent_required.
2. Обменяйте код
curl -X POST https://auth.aperturesyndicate.com/api/v1/sso/token \
-H "content-type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "asc_…",
"code": "…",
"redirect_uri": "https://yourapp.example/callback",
"code_verifier": "…"
}'{
"access_token": "eyJ…",
"refresh_token": "…",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile"
}Эндпоинт принимает JSON или application/x-www-form-urlencoded. Запрос формой получает ошибки в формате RFC 6749 ({"error":"invalid_grant","error_description":"…"}), запрос в JSON — {"error":"…","code":"…"}.
3. Обновление
Access-токен живёт 15 минут, refresh-токен — 30 дней. Каждое обновление выдаёт новый refresh-токен: сохраните его, а старый выбросьте.
{ "grant_type": "refresh_token", "client_id": "asc_…", "refresh_token": "…" }Refresh-токен годится только тому клиенту, которому выдан. Если человек вышел на всех устройствах, заблокирован или удалил аккаунт, обновление не пройдёт, и ему придётся войти заново.
4. Кто вошёл
curl https://auth.aperturesyndicate.com/api/v1/sso/userinfo \
-H "Authorization: Bearer eyJ…"{
"sub": "5f0c…",
"nickname": "Nova",
"avatar_url": "https://…",
"profile_url": "https://aperturesyndicate.com/u/nova",
"email": "…",
"email_verified": true
}Со scope subscription:
"subscription": {
"plan": "PREMIUM",
"status": "ACTIVE",
"features": ["lq"],
"items": [],
"current_period_end": "2026-11-29T00:00:00.000Z",
"cancel_at_period_end": false
}Проверяйте features, а не plan — plan оставлен для старых клиентов. status равен CANCELLED, если подписка дорабатывает период и не продлится.
Проверка токена у себя
Access-токен — JWT на RS256. Проверяйте его по JWKS, а не вызовом /userinfo на каждый запрос:
| Поле | |
|---|---|
iss | https://auth.aperturesyndicate.com |
aud | as-third-party и ваш client_id — токен без вашего client_id отклоняйте |
sub | Id аккаунта |
nickname | Отображаемое имя на момент выдачи |
scopes | Выданные scopes |
exp, iat | Срок и время выдачи |
Токен, выданный вашему приложению, открывает только /userinfo. Это не ключ к плейлистам человека, ASAI или чему-то ещё на платформе.
Ошибки
| Код | Где | Значит |
|---|---|---|
INVALID_REQUEST | оба | Параметра нет или он неверный, в том числе PKCE plain |
INVALID_CLIENT | оба | Неизвестный или отключённый client_id |
INVALID_REDIRECT_URI | оба | Не заведён или отличается от того, что был в шаге 1 |
auth.state.missing | authorize | Не передан state |
INVALID_CODE | token | Код истёк, использован или выдан другому клиенту |
PKCE_FAILED | token | code_verifier не совпадает с challenge |
INVALID_SCOPE | token | Scope вне списка клиента |
INVALID_TOKEN | token | Refresh-токен истёк или отозван |
INVALID_GRANT | token | Refresh-токен принадлежит другому клиенту |
ACCOUNT_BANNED | token | 403; все сессии аккаунта закрыты |
MISSING_TOKEN, TOKEN_REVOKED | userinfo | 401 — нет токена или человек вышел |