🔐 RuAuth

Интеграция RuAuth-2FA на вашем сайте

Руководство для разработчиков-партнёров. Ключи для доступа берутся в личном кабинете (проект → API_KEY и API_SECRET).

Как это устроено

Все запросы идут с бэкенда вашего сайта (не из браузера) по HTTPS.

Шаг 0. Получить доступ

В кабинете создайте проект — вы получите два значения:

API_SECRET храните только на сервере (переменная окружения / секрет-хранилище), никогда не отдавайте в браузер и не коммитьте в репозиторий. Показывается один раз; при утере — выпустите новый в кабинете.

Подпись запросов

К каждому POST-запросу добавляйте заголовки:

Важно:

Примеры подписи и вызова

PHP
function 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 })
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": "…"}
У пользователя нет 2FA404 {"error": "…"}
Слишком много попыток429 {"error": "…"} — временная блокировка

Обрабатывайте 429 как «попробуйте позже», не зацикливайте повтор.

Чек-лист безопасности