Интеграция RuAuth-2FA на вашем сайте
Руководство для разработчиков-партнёров. Ключи для доступа берутся в личном кабинете (проект → API_KEY и API_SECRET).
Как это устроено
- RuAuth (наш сервер) генерирует и хранит секреты 2FA, проверяет коды.
- Ваш сайт хранит у себя только:
user_id— ваш стабильный идентификатор пользователя (id из вашей БД, e-mail и т.п.);twofa_enabled— булев флаг «2FA включена».
- Секрет 2FA у вас не хранится — он живёт в RuAuth. Вы обращаетесь к API по
user_id.
Все запросы идут с бэкенда вашего сайта (не из браузера) по HTTPS.
Шаг 0. Получить доступ
В кабинете создайте проект — вы получите два значения:
API_KEY— идентификатор проекта (напримерpk_…);API_SECRET— длинный секрет для подписи запросов.
API_SECRET храните только на сервере (переменная окружения /
секрет-хранилище), никогда не отдавайте в браузер и не коммитьте в репозиторий.
Показывается один раз; при утере — выпустите новый в кабинете.Подпись запросов
К каждому POST-запросу добавляйте заголовки:
Content-Type: application/jsonX-Api-Key: <API_KEY>X-Timestamp: <unix-время в секундах>X-Signature: hex(HMAC_SHA256("<X-Timestamp>." + <точные байты тела>, <API_SECRET>))
Важно:
- Подписывается строка
timestamp + "." + тело. Подписывайте ровно те байты тела, что отправляете. Формат JSON не важен — важно совпадение подписанного и отправленного. X-Timestampобязателен и должен отличаться от времени сервера не более чем на ±60 секунд (следите за часами сервера). Это защита от повторного проигрывания запроса; повтор той же подписи в пределах окна тоже отклоняется.
Примеры подписи и вызова
PHPfunction ruauth(string $path, array $payload): array {
$body = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$ts = (string) time();
$sig = hash_hmac('sha256', $ts . '.' . $body, getenv('RUAUTH_API_SECRET'));
$ch = curl_init(getenv('RUAUTH_URL') . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Api-Key: ' . getenv('RUAUTH_API_KEY'),
'X-Timestamp: ' . $ts,
'X-Signature: ' . $sig,
],
]);
$resp = curl_exec($ch);
return json_decode($resp, true) ?? [];
}
Python
import hmac, hashlib, json, os, time, requests
def ruauth(path, payload):
body = json.dumps(payload, separators=(",", ":")).encode()
ts = str(int(time.time()))
sig = hmac.new(os.environ["RUAUTH_API_SECRET"].encode(),
(ts + ".").encode() + body, hashlib.sha256).hexdigest()
r = requests.post(os.environ["RUAUTH_URL"] + path, data=body, headers={
"Content-Type": "application/json",
"X-Api-Key": os.environ["RUAUTH_API_KEY"],
"X-Timestamp": ts,
"X-Signature": sig,
}, timeout=10)
return r.json()
Node.js
import crypto from "node:crypto";
async function ruauth(path, payload) {
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000).toString();
const sig = crypto.createHmac("sha256", process.env.RUAUTH_API_SECRET)
.update(ts + "." + body).digest("hex");
const r = await fetch(process.env.RUAUTH_URL + path, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.RUAUTH_API_KEY,
"X-Timestamp": ts,
"X-Signature": sig,
},
body,
});
return r.json();
}
Go
func callRuAuth(path string, payload any) (map[string]any, int, error) {
body, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, cfg.ruauth+path, bytes.NewReader(body))
if err != nil {
return nil, 0, err
}
ts := strconv.FormatInt(time.Now().Unix(), 10)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Api-Key", cfg.apiKey)
req.Header.Set("X-Timestamp", ts)
req.Header.Set("X-Signature", sign(ts, body)) // подпись "ts.тело" секретом проекта
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
var out map[string]any
_ = json.Unmarshal(raw, &out)
return out, resp.StatusCode, nil
}
func sign(ts string, body []byte) string {
mac := hmac.New(sha256.New, []byte(cfg.apiSecret))
mac.Write([]byte(ts + "."))
mac.Write(body)
return hex.EncodeToString(mac.Sum(nil))
}
Сценарии
1. Включение 2FA (в настройках безопасности)
resp = ruauth("/v1/enroll", { "user_id": U, "issuer": "Ваш Сервис", "account_name": email })
- Ответ:
otpauth_uri,secret,qr_png_base64. - Покажите QR:
<img src="data:image/png;base64, …qr_png_base64… ">(или отрисуйтеotpauth_uri). - Пользователь сканирует его приложением RuAuth и вводит 6-значный код.
- Подтвердите код:
resp = ruauth("/v1/verify", { "user_id": U, "code": "123456" })
if resp.valid: сохранить twofa_enabled = true у себя в БД
⚠️
enroll каждый раз выдаёт новый секрет и перезаписывает
прежний. Вызывайте его только когда пользователь начинает подключение 2FA, а не для уже
включённых. Для уже подтверждённой 2FA сервер вернёт 409 без reset=true.2. Вход (второй фактор)
После проверки пароля, если у пользователя twofa_enabled:
resp = ruauth("/v1/verify", { "user_id": U, "code": введённый_код })
if resp.valid: впускаем; иначе — «неверный код»
Один и тот же код повторно не примут (защита от replay) — это нормально.
3. Резервные коды (на случай потери телефона)
При включении 2FA сгенерируйте и покажите один раз:
resp = ruauth("/v1/recovery/generate", { "user_id": U, "count": 10 })
// resp.codes — список одноразовых кодов
Проверка резервного кода (альтернатива обычному коду при входе):
resp = ruauth("/v1/recovery/verify", { "user_id": U, "code": "abcde-12345" })
4. Отключение 2FA
ruauth("/v1/disable", { "user_id": U })
// затем twofa_enabled = false у себя
Коды ответов и ошибки
| Ситуация | Что приходит |
|---|---|
| Код верный / резервный погашен | 200 {"valid": true} |
| Код неверный | 200 {"valid": false} |
| Неверная подпись или API-ключ | 401 {"error": "…"} |
| У пользователя нет 2FA | 404 {"error": "…"} |
| Слишком много попыток | 429 {"error": "…"} — временная блокировка |
Обрабатывайте 429 как «попробуйте позже», не зацикливайте повтор.
Чек-лист безопасности
- ☐
API_SECRET— только на бэкенде, в переменных окружения; не в браузере, не в git. - ☐ Все вызовы к RuAuth — по HTTPS (
https://api.ru2fa.ru). - ☐ Не логируйте введённые коды и секреты.
- ☐
user_id— стабильный (не меняется у пользователя со временем). - ☐
enrollвызывается только на этапе подключения, не для уже включённых пользователей. - ☐ Резервные коды показываются пользователю один раз и хранятся им, не вами.