Voice ID — кнопка «Войти через Voice» на вашем сайте. Это стандартный OAuth 2.0 (authorization code) с PKCE и OpenID Connect: сайт получает только то, что разрешил пользователь, а пароль от Voice не видит никогда.
1. Настройте приложение
В консоли → приложение → «Voice ID» добавьте адреса возврата (redirect_uri, до 10). Совпадение должно быть точным: схема, домен, порт и путь. Для сервера сайта нужен client_secret: он показывается один раз — при создании приложения и при сбросе на той же вкладке. client_id — это ID приложения.
2. Отправьте пользователя на экран входа
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обяз. | string | ID приложения. |
redirect_uriобяз. | string | Один из зарегистрированных адресов. Можно не передавать, если адрес ровно один. |
response_typeобяз. | code | Поддерживается только code. |
scopeобяз. | string | Права через пробел (см. ниже). |
state | string | Случайная строка: вернётся без изменений — сверьте её, это защита от CSRF. |
code_challenge | string | PKCE: BASE64URL(SHA-256(code_verifier)). Обязателен для приложений без секрета (SPA, мобильные). |
code_challenge_method | S256 | Поддерживается только S256. |
nonce | string | Для openid: попадёт в id_token. |
Права (scope)
| scope | Что получает приложение |
|---|---|
identify | id, ник, имя и аватар. Нужен для /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:
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 вместо секрета. Так входит в консоль и этот сайт.
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 | Тип | Описание |
|---|---|---|
sub | string | ID пользователя Voice — постоянный. |
preferred_username, name, picture | string | Ник, отображаемое имя, аватар. |
email, email_verified | string, boolean | Только со scope email. |
nonce | string | Из запроса авторизации. |
iat, exp | number | Выдан и истекает (через 1 час). |
Полный список методов — в справочнике OAuth2.