OAuth 2.0 + PKCE
Стандартный OAuth flow для подключения сторонних приложений и MCP-клиентов (Claude Desktop, Cursor, Cline) без копирования root-ключа.
Hubris реализует OAuth 2.0 Authorization Code + PKCE (RFC 6749 + 7636 + 9700 BCP) для авторизации сторонних приложений. Это даёт два преимущества:
- Sandboxed-доступ: токен выдаётся под выбранные scopes и опциональный дневной лимит. Если приложение попросит чат — оно не сможет читать ваш баланс.
- MCP-connector совместимость: Claude Desktop, Claude.ai, Cursor и любой MCP-клиент с native «Add custom connector» UI проходят discovery + DCR + авторизацию автоматически, без
mcp-remoteшима.
Discovery (RFC 8414 + RFC 9728)
Клиент начинает с одного из:
GET https://api.hubris.pw/.well-known/oauth-protected-resource
GET https://hubris.pw/.well-known/oauth-authorization-serverВторой — основной, содержит все endpoints. Пример ответа:
{
"issuer": "https://hubris.pw",
"authorization_endpoint": "https://hubris.pw/oauth/authorize",
"token_endpoint": "https://hubris.pw/oauth/token",
"registration_endpoint": "https://hubris.pw/oauth/clients",
"revocation_endpoint": "https://hubris.pw/oauth/revoke",
"scopes_supported": ["chat:write", "models:read", "balance:read"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}token_endpoint_auth_methods_supported: ["none"] означает что мы поддерживаем только public clients (PKCE-only, без client_secret).
Scopes
| Scope | Доступ |
|---|---|
chat:write | /v1/chat/completions, /v1/responses, /v1/messages, /mcp chat_complete |
models:read | /v1/models, /mcp models_list+search+get_pricing |
balance:read | чтение баланса (/api/internal/me, /mcp balance_get) |
Приложение запрашивает подмножество в ?scope=... (space-separated). Юзер видит и подтверждает на consent-странице.
Токены
- Access token: формат
hbr-at-<32 hex>. TTL 1 час. Идёт какAuthorization: Bearer ...на/v1/*и/mcp. - Refresh token: формат
hbr-rt-<48 hex>. TTL 90 дней. Принимается только наPOST /oauth/token. - Rotation: каждый refresh выпускает новый refresh + новый access. Старый refresh имеет 30-секундное grace-окно (защита от race-conditions при параллельных запросах). После окна — обнаружение reuse и revoke всей семьи токенов.
Поток (Python пример)
import requests, hashlib, base64, secrets
# 1. Регистрация client (one-time per app)
reg = requests.post('https://hubris.pw/oauth/clients', json={
'client_name': 'My App',
'redirect_uris': ['http://localhost:8765/callback'],
'scope': 'chat:write models:read',
})
client_id = reg.json()['client_id']
# 2. PKCE
verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b'=').decode()
# 3. Браузерный flow — открыть в браузере
auth_url = (
'https://hubris.pw/oauth/authorize'
f'?client_id={client_id}'
'&redirect_uri=http://localhost:8765/callback'
'&response_type=code'
f'&code_challenge={challenge}&code_challenge_method=S256'
'&scope=chat:write+models:read'
'&state=somestate'
)
# Юзер видит consent, кликает «Разрешить» → редирект на callback?code=...&state=...
# 4. Обмен кода на токены
tok = requests.post('https://hubris.pw/oauth/token', data={
'grant_type': 'authorization_code',
'code': received_code,
'redirect_uri': 'http://localhost:8765/callback',
'client_id': client_id,
'code_verifier': verifier,
}).json()
access_token = tok['access_token']
# 5. Используем
resp = requests.post(
'https://api.hubris.pw/v1/chat/completions',
headers={'Authorization': f'Bearer {access_token}'},
json={
'model': 'openai/gpt-4o-mini',
'messages': [{'role': 'user', 'content': 'привет'}],
},
)Дневной лимит
Приложение может передать daily_limit_kopecks=50000 в query к /oauth/authorize. Юзер видит запрошенную сумму на consent-странице и может снизить перед подтверждением. Если приложение не передало — default 500 ₽/день.
После выдачи токена лимит работает идентично api_keys daily-limit'у — 429 при превышении с метаданными в /usage.
Безопасность
Hubris следует RFC 9700 BCP:
- PKCE S256 обязательно —
plainметод не поддерживается. - Refresh rotation + reuse detection — обнаруженный replay старого refresh после grace-окна revoke'ит всю семью токенов.
- redirect_uri exact-match после нормализации (lowercase scheme/host, case-sensitive path) — без prefix-wildcards.
- Reserved-word check на client_name (
hubris,official,admin,supportзапрещены как substring) — anti-phishing baseline. - DCR rate-limit: 5 регистраций/IP/час + 100/день глобально.
- Все токены — sha256-хэши в БД. Plain text никогда не хранится.
MCP integration
Совместимость с native «Add custom connector» UI в Claude Desktop / Claude.ai:
- В клиенте указываешь URL:
https://api.hubris.pw/mcp - Клиент сам идёт по discovery → DCR → /authorize (открывает браузер)
- Юзер видит consent: «Claude Desktop запрашивает доступ chat:write, models:read, balance:read»
- После «Разрешить» — обратно в клиент, токены выпущены, инструменты подключены
Legacy sk-gw- ключи продолжают работать на /mcp без изменений (back-compat).
Phase B (deferred)
/profile/connected-appsUI — список авторизованных приложений + revoke- Per-scope deny (сейчас all-or-nothing на consent-странице)
- Step-up auth (re-OTP при выдаче sensitive scopes если auth >7д назад)
- Domain verification для verified-badge при DCR
embeddings:write,usage:readscopes- OAuth для /v1/embeddings + /v1/usage routes
Ссылки
- RFC 6749 — OAuth 2.0
- RFC 7009 — Token Revocation
- RFC 7591 — Dynamic Client Registration
- RFC 7636 — PKCE
- RFC 8414 — Authorization Server Metadata
- RFC 9700 — OAuth 2.0 Security Best Current Practice
- RFC 9728 — Protected Resource Metadata
- MCP Authorization: https://modelcontextprotocol.io/specification/draft/basic/authorization