Аутентификация

Чтобы взаимодействовать с API сервиса, необходимо аутентифицироваться в системе.

Для аутентификации запросов API использует специальные ключи доступа — токены, передаваемые в заголовке запроса. Использование токена позволяет избавиться от передачи имени пользователя и пароля в открытом виде и обеспечивает их защиту при работе с сервисом. Пароль пользователя известен только клиенту.

Формирование токена выполняет клиент с помощью хеширования данных учётной записи и меток времени.

Каждый запрос к API должен содержать HTTP-заголовок Authorization со значением AR-REST <token>.

Формат токена

Токен имеет формат base64(user:stamp:age:salted_hash), где:

  • user — UID пользователя в формате name@domain;
  • stamp:age — метка времени:
    • stamp — дата начала действия токена с точностью до секунды (UTC + 0 в Unix-time);
    • age — время жизни токена в секундах;
  • salted_hash — солёный хеш пароля base64(md5(salt:pass_hash)):
    • pass_hash = base64(md5(pass)) — хешированный пароль pass пользователя;
    • salt — метка времени stamp:age.

Алгоритм генерации токена

{primary.fa-terminal} Для отладки формирования токена используйте команду Swagger /dev/token.

Не передавайте в Swagger или другие внешние сервисы данные своей учётной записи в незашифрованном виде: они могут быть перехвачены и прочитаны злоумышленниками.

  1. Подготовьте исходные данные. Для примера будут использоваться следующие значения:
    user = test_user@test_domain
    pass = 123
    stamp = 1483634723 (2017-01-05T16:45:23.000Z)
    age = 999999999
  2. Сформируйте pass_hash:
    • получите бинарный MD5-хеш строки пароля md5(pass)
    • выполните конвертацию хеша в Base64: ICy5YqxZB1uWSwcVLSNLcA==

      {primary.fa-info} Для кодирования в Base64 следует использовать бинарное представление хеша (сырой массив из 16 байт).

      Онлайн-сервисы хеширования обычно возвращают ответ в виде строки, содержащей шестнадцатеричное число. При конвертации такого числа в Base64 убедитесь, что тип исходных данных — шестнадцатеричное число, а не текст.

      Проверить корректность формирования pass_hash можно, используя сервисы Online-Convert или Cryptii.

  3. Сформируйте salted_hash:
    • к строке соли stamp:age добавьте pass_hash: 1483634723:999999999:ICy5YqxZB1uWSwcVLSNLcA==
    • получите бинарный хеш md5(pass_hash) и сконвертируйте его в Base64: 3wg82EuTwec29/OvQ7myyA==
  4. Сформируйте токен token:
    • соберите строку user:stamp:age:salted_hash: test_user@test_domain:1483634723:999999999:3wg82EuTwec29/OvQ7myyA==
    • сконвертируйте её в Base64: dGVzdF91c2VyQHRlc3RfZG9tYWluOjE0ODM2MzQ3MjM6OTk5OTk5OTk5OjN3ZzgyRXVUd2VjMjkvT3ZRN215eUE9PQ==

Используйте сгенерированный токен в HTTP-заголовке Authorization:

Authorization: AR-REST dGVzdF91c2VyQHRlc3RfZG9tYWluOjE0ODM2MzQ3MjM6OTk5OTk5OTk5OjN3ZzgyRXVUd2VjMjkvT3ZRN215eUE9PQ==

Как выбрать время жизни токена

Время жизни токена — период, в течение которого он является действительным. Ограниченный срок жизни позволяет более надёжно защитить отправляемые запросы. Даже в случае кражи токена злоумышленник не сможет выполнить запросы с ним после окончания его срока действия. Чем меньше время жизни токена, тем более безопасным он является.

{primary.fa-info} Мы рекомендуем использовать минимальное время жизни токена, но не менее 30 секунд. Такой период снижает риск потери запроса из-за задержек сети или рассинхронизации времени сервера и клиента.

В качестве даты начала действия токена можно использовать текущее время.

Использование токенов с длительным сроком жизни и отказ от генерации новых токенов для каждого запроса не является безопасным и увеличивает вероятность компрометации данных.

Пример кода получения токена

Go

package main

import (
  "crypto/md5"
  "encoding/base64"
  "fmt"
  "time"
)

func generateToken(uid, password string, age time.Duration) (string, error) {
  hasher := md5.New()
  if _, err := hasher.Write([]byte(password)); err != nil {
    return "", err
  }

  passHash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))

  hasher.Reset()

  stamp := time.Now().Unix()
  ttl := int(age.Seconds())

  salted := fmt.Sprintf("%d:%d:%s", stamp, ttl, passHash)
  if _, err := hasher.Write([]byte(salted)); err != nil {
    return "", err
  }

  saltedPassHash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))

  tokenRaw := fmt.Sprintf("%s:%d:%d:%s", uid, stamp, ttl, saltedPassHash)
  return base64.StdEncoding.EncodeToString([]byte(tokenRaw)), nil
}

func main() {
  token, err := generateToken("test@test", "SomeStrongPa$$w0rd", 24 * time.Hour)

  if err != nil {
    panic(err)
  }

  fmt.Println(token)
}

Python

from base64 import b64encode
from hashlib import md5
from time import time

def generate_token(user: str, password: str, age: int = 60 * 60 * 24) -> str:
    timestamp = int(time())

    password_hash = b64encode(md5(password.encode()).digest()).decode()

    hash_with_salt = f"{timestamp}:{age}:{password_hash}"
    salted_hash_b64 = b64encode(md5(hash_with_salt.encode()).digest()).decode()

    token = f"{user}:{timestamp}:{age}:{salted_hash_b64}"
    token_b64 = b64encode(token.encode()).decode()

    return token_b64

if __name__ == '__main__':
    api_token = generate_token(
        user='test_user@test_domain',
        password='123',
        age=999999999
    )
    print(api_token)

PHP

$user = $request->get('uid');
$pass = $request->get('password');
$stamp = time();
$age = 60*60*24;
$passHash = base64_encode(md5($pass, true));
$saltedHash = base64_encode(md5($stamp.':'.$age.':'.$passHash, true));
$token = base64_encode(implode(':', [$user, $stamp, $age, $saltedHash]));

Java

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Base64;

public void genTokenExamle() throws ParseException, NoSuchAlgorithmException {

  String user = "test@test";
  String pass = "123";
  long stamp = (new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX")).parse("2017-01-05T16:45:23Z").getTime() / 1000;
  long age = 999999999;

  MessageDigest md = MessageDigest.getInstance("MD5");
  String pass_hash = Base64.getEncoder().encodeToString(md.digest(pass.getBytes()));

  String salt = stamp + ":" + age;
  String salt_hash = Base64.getEncoder().encodeToString(md.digest((salt + ":" + pass_hash).getBytes()));

  String token = Base64.getEncoder().encodeToString((user + ":" + stamp + ":" + age + ":" + salt_hash).getBytes());

  String header = "AR-REST " + token;

  System.out.println(header);
}

Ошибки аутентификации

Запросы, использующие недействительные токены (сформированные некорректно или использующие неверные данные учётной записи) будут обработаны с ошибкой. API возвращает объект ошибки Security и её описание. Все возможные ошибки обработки запросов указаны в разделе Ошибки обработки запросов.