OAuth PKCE: API-ключ для вашего приложения
OAuth PKCE позволяет приложению получить API-ключ пользователя без ручного копирования. Пользователь входит в RouterAI, выбирает аккаунт, название ключа и лимит расходов. Приложение получает обычный ключ sk-….
Регистрировать приложение не нужно. Пути и поля совпадают с OpenRouter, поэтому приложению с «Login with OpenRouter» достаточно заменить хост на https://routerai.ru.
Как это работает
- Приложение генерирует
code_verifierиcode_challenge. - Перенаправляет пользователя на
https://routerai.ru/auth. - Пользователь подтверждает выдачу ключа.
- RouterAI возвращает пользователя на
callback_urlс параметромcode. - Приложение отправляет
codeиcode_verifierнаPOST https://routerai.ru/api/v1/auth/keysи получает ключ. - Ключ работает как обычный:
https://routerai.ru/api/v1, заголовокAuthorization: Bearer sk-….
Шаг 1. code_verifier и code_challenge
code_verifier — случайная строка из 43–128 символов [A-Za-z0-9-._~]. code_challenge — BASE64URL(SHA256(code_verifier)) без = на конце. code_verifier храните у себя и не передавайте в URL.
import { randomBytes, createHash } from 'node:crypto';
const codeVerifier = randomBytes(32).toString('base64url');
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url');
import base64, hashlib, secrets
code_verifier = secrets.token_urlsafe(32)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b"=").decode()
const b64url = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const codeVerifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const codeChallenge = b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)));
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '+/' '-_')
CODE_CHALLENGE=$(printf %s "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '+/' '-_')
Шаг 2. Перенаправление на /auth
https://routerai.ru/auth?callback_url=https://example.com/callback&code_challenge=…&code_challenge_method=S256&state=…&app_name=My%20App
| Параметр | Обязательный | Описание |
|---|---|---|
callback_url |
нет | Адрес возврата. Только https, http допускается лишь для localhost/127.0.0.1 (любой порт). Можно указать собственную схему приложения (myapp://callback). Фрагмент # запрещён. Без параметра включается headless-режим. |
code_challenge |
да | BASE64URL(SHA256(code_verifier)), 43 символа |
code_challenge_method |
да | Только S256. plain не поддерживается |
state |
нет | До 256 символов, возвращается без изменений. Защищает от CSRF: сверьте его перед обменом кода |
app_name |
нет | Название приложения на экране согласия |
key_label |
нет | Название ключа по умолчанию, пользователь может изменить |
limit |
нет | Лимит расходов по умолчанию, ₽ (0 — без лимита). Если не задан — 100 ₽ |
limit_period |
нет | day, week или month. Если не задан — day |
Если в запросе ошибка (например, нет code_challenge или указан недопустимый callback_url), RouterAI показывает её на своей странице и не возвращает пользователя в приложение.
Шаг 3. Callback
- Успех:
callback_url?code=…&state=…&iss=https://routerai.ru - Отказ:
callback_url?error=access_denied&state=…
Сверьте state с сохранённым значением. Код действует 5 минут и используется один раз.
Шаг 4. Обмен кода на ключ
curl -X POST https://routerai.ru/api/v1/auth/keys \
-H "Content-Type: application/json" \
-d '{
"code": "…",
"code_verifier": "…",
"code_challenge_method": "S256",
"callback_url": "https://example.com/callback"
}'
const response = await fetch('https://routerai.ru/api/v1/auth/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
code_verifier: codeVerifier,
code_challenge_method: 'S256',
callback_url: 'https://example.com/callback',
}),
});
const { key } = await response.json();
import requests
response = requests.post(
"https://routerai.ru/api/v1/auth/keys",
json={
"code": code,
"code_verifier": code_verifier,
"code_challenge_method": "S256",
"callback_url": "https://example.com/callback",
},
)
response.raise_for_status()
key = response.json()["key"]
Ответ:
{ "key": "sk-…" }
callback_url передавайте тот же, что и в /auth, символ в символ. code_challenge_method необязателен, но если он указан, допустимо только S256. Эндпоинт поддерживает CORS, поэтому обмен можно выполнить прямо из браузера.
Ошибки ({"error": "…", "error_description": "…"}):
| HTTP | error |
Причина |
|---|---|---|
| 400 | invalid_grant |
Код не найден, истёк или уже использован; callback_url не совпадает; неверный code_verifier; у пользователя больше нет прав на выбранный аккаунт |
| 400 | invalid_request |
code_challenge_method отличается от S256 |
| 429 | slow_down |
Больше 30 запросов в минуту с одного IP |
Если уже использованный код предъявить повторно вместе с верным code_verifier, выданный по нему ключ выключается: значит, утекла вся пара. Повтор одного кода без верного code_verifier ключ не трогает.
Headless-режим
Для CLI и приложений без браузерного callback откройте /auth без callback_url. После подтверждения RouterAI покажет код на экране. Пользователь вставляет его в приложение, и приложение обменивает код через POST /api/v1/auth/keys без callback_url. Правила те же: код действует 5 минут и используется один раз.
Повторная авторизация
Если тот же пользователь снова авторизует то же приложение для того же аккаунта, ключ перевыпускается. Старый секрет сразу перестаёт работать, а название и лимит обновляются. Дубликат не создаётся, статистика ключа сохраняется.
Приложение определяется так:
- веб-приложение — по адресу (
https://example.com) изcallback_url; localhostи headless — поapp_name.
Ключи localhost- и headless-приложений без app_name не перевыпускаются: каждый раз создаётся новый ключ.
Лимиты и управление ключом
Лимит расходов действует как у обычного ключа: при его достижении или нехватке баланса запросы отклоняются. Пользователь видит ключ в Настройках → API-ключи с пометкой «OAuth». Там можно изменить лимит, выключить или удалить ключ.