Руководства

Войти через Voice (OAuth2 и OpenID Connect)

Voice ID — кнопка «Войти через Voice» на вашем сайте. Это стандартный OAuth 2.0 (authorization code) с PKCE и OpenID Connect: сайт получает только то, что разрешил пользователь, а пароль от Voice не видит никогда.

Любая OAuth/OIDC-библиотека подойдёт: настройки берутся из https://voice-chat.ru/.well-known/openid-configuration.

1. Настройте приложение

В консоли → приложение → «Voice ID» добавьте адреса возврата (redirect_uri, до 10). Совпадение должно быть точным: схема, домен, порт и путь. Для сервера сайта нужен client_secret: он показывается один раз — при создании приложения и при сбросе на той же вкладке. client_id — это ID приложения.

2. Отправьте пользователя на экран входа

HTTP
GET https://voice-chat.ru/oauth2/authorize?client_id=ID&redirect_uri=https%3A%2F%2Fexample.com%2Fauth%2Fvoice&response_type=code&scope=identify%20email&state=СЛУЧАЙНАЯ_СТРОКА&code_challenge=…&code_challenge_method=S256
ПараметрЗначениеОписание
client_idобяз.stringID приложения.
redirect_uriобяз.stringОдин из зарегистрированных адресов. Можно не передавать, если адрес ровно один.
response_typeобяз.codeПоддерживается только code.
scopeобяз.stringПрава через пробел (см. ниже).
statestringСлучайная строка: вернётся без изменений — сверьте её, это защита от CSRF.
code_challengestringPKCE: BASE64URL(SHA-256(code_verifier)). Обязателен для приложений без секрета (SPA, мобильные).
code_challenge_methodS256Поддерживается только S256.
noncestringДля openid: попадёт в id_token.

Права (scope)

scopeЧто получает приложение
identifyid, ник, имя и аватар. Нужен для /api/oauth2/userinfo и /api/users/@me.
emailПочта и подтверждена ли она.
serversСписок серверов пользователя: GET /api/users/@me/servers.
openidПодписанный id_token (OpenID Connect).
botДобавить бота приложения на сервер (+ параметр permissions). Токен не выдаётся.
applications.commandsКоманды «/» бота на сервере — вместе с bot.

Пользователь видит экран «Разрешить доступ?» с вашим названием, иконкой и списком прав. Если он уже разрешал те же права, вход проходит сразу. После ответа Voice возвращает его на redirect_uri:

text
https://example.com/auth/voice?code=vac_…&state=СЛУЧАЙНАЯ_СТРОКА
https://example.com/auth/voice?error=access_denied&error_description=…&state=…

Код одноразовый и живёт 5 минут.

3. Обменяйте код на токен

POST https://voice-chat.ru/api/oauth2/token, тело application/x-www-form-urlencoded. Приложение подтверждает себя секретом (Basic-заголовок или client_secret в теле) либо, без секрета, — code_verifier из PKCE.

// Обработчик redirect_uri на сервере сайта (Express)
app.get("/auth/voice", async (req, res) => {
  if (req.query.state !== req.session.voiceState) return res.status(400).send("state не совпал");
  const r = await fetch("https://voice-chat.ru/api/oauth2/token", {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: String(req.query.code),
      redirect_uri: "https://example.com/auth/voice",
      client_id: process.env.VOICE_CLIENT_ID,
      client_secret: process.env.VOICE_CLIENT_SECRET,
      code_verifier: req.session.voiceVerifier, // если использовали PKCE
    }),
  });
  const tokens = await r.json();
  if (!r.ok) return res.status(400).send(tokens.error_description);

  const user = await fetch("https://voice-chat.ru/api/oauth2/userinfo", {
    headers: { Authorization: `Bearer ${tokens.access_token}` },
  }).then((r) => r.json());
  req.session.user = { voiceId: user.sub, name: user.name, avatar: user.picture };
  res.redirect("/");
});
Ответ
{
  "access_token": "vat_…",
  "refresh_token": "vrt_…",
  "token_type": "Bearer",
  "expires_in": 604800,
  "scope": "identify email",
  "id_token": "eyJ…"   // только со scope openid
}

PKCE для приложений без сервера

В браузерном (SPA) или мобильном приложении секрет хранить нельзя. Создайте случайный code_verifier (43–128 символов), отправьте его хэш в code_challenge, а при обмене кода передайте сам code_verifier вместо секрета. Так входит в консоль и этот сайт.

JavaScript
const b64url = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)))
  .replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");

const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
const state = b64url(crypto.getRandomValues(new Uint8Array(16)));
sessionStorage.setItem("voice_pkce", JSON.stringify({ verifier, state }));

location.assign("https://voice-chat.ru/oauth2/authorize?" + new URLSearchParams({
  client_id: CLIENT_ID,
  redirect_uri: location.origin + "/callback",
  response_type: "code",
  scope: "identify",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
}));

// На /callback:
const p = new URLSearchParams(location.search);
const saved = JSON.parse(sessionStorage.getItem("voice_pkce"));
if (p.get("state") !== saved.state) throw new Error("state не совпал");
const tokens = await fetch("https://voice-chat.ru/api/oauth2/token", {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code", client_id: CLIENT_ID, code: p.get("code"),
    redirect_uri: location.origin + "/callback", code_verifier: saved.verifier,
  }),
}).then((r) => r.json());

Обновление и отзыв

Токен доступа живёт 7 дней, токен обновления — 60 дней. Обновление одноразовое: в ответ приходит новая пара, старая перестаёт работать.

Терминал
curl -X POST https://voice-chat.ru/api/oauth2/token -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d grant_type=refresh_token -d refresh_token=vrt_…

# Выход: отозвать токен (ответ всегда {})
curl -X POST https://voice-chat.ru/api/oauth2/token/revoke -d token=vat_…

Пользователь может отозвать доступ сам: «Настройки → Безопасность → Voice ID». Сброс client_secret отзывает все токены приложения.

id_token (OpenID Connect)

Со scope openid в ответе есть id_token — JWT с подписью RS256. Ключи — https://voice-chat.ru/api/oauth2/keys (JWKS). Проверьте подпись, iss = https://voice-chat.ru, aud = ваш client_id, exp и nonce.

ClaimТипОписание
substringID пользователя Voice — постоянный.
preferred_username, name, picturestringНик, отображаемое имя, аватар.
email, email_verifiedstring, booleanТолько со scope email.
noncestringИз запроса авторизации.
iat, expnumberВыдан и истекает (через 1 час).

Полный список методов — в справочнике OAuth2.