340 lines
13 KiB
Markdown
340 lines
13 KiB
Markdown
# API для разработчиков: Авторизация
|
||
|
||
Этот файл описывает именно этапы авторизации клиента, то есть как создать новую сессию и как войти в уже существующую.
|
||
|
||
Здесь четыре базовых метода обычной авторизации:
|
||
|
||
- `AuthChallenge`
|
||
- `CreateAuthSession`
|
||
- `SessionChallenge`
|
||
- `SessionLogin`
|
||
|
||
Логика раздела такая:
|
||
|
||
- сначала клиент либо начинает создание новой сессии через `deviceKey`;
|
||
- либо начинает вход в уже созданную сессию через `sessionKey`;
|
||
- сервер на первом шаге выдаёт challenge/nonce;
|
||
- на втором шаге клиент присылает подписанный ответ;
|
||
- сервер сверяет актуальные публичные ключи и только потом проверяет подпись.
|
||
|
||
Новые поля этого раздела:
|
||
|
||
- `sessionType` — числовой код типа сессии;
|
||
- `clientPlatform` — свободная строка платформы клиента.
|
||
|
||
Текущие поддерживаемые коды `sessionType`:
|
||
|
||
- `1` — обычный клиент;
|
||
- `50` — кошелёк;
|
||
- `100` — homeserver.
|
||
|
||
Правило проверки `sessionType`:
|
||
|
||
1. если в `Solana PDA` нет записи для `sessionKey`, сервер принимает `sessionType`, присланный клиентом;
|
||
2. если запись в `PDA` есть, `sessionType` в запросе должен совпадать с `session_type` из `PDA`;
|
||
3. при несовпадении сервер возвращает `460 / SESSION_TYPE_MISMATCH`.
|
||
|
||
Ниже в документе сначала описан сценарий, а потом зафиксированы точные форматы запросов и ответов.
|
||
|
||
Отдельно появился новый серверный сценарий pairing через доверенный homeserver/ESP. Он не заменяет обычный вход и описан в:
|
||
|
||
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
|
||
|
||
Кратко:
|
||
|
||
- `AuthChallenge/CreateAuthSession` и `SessionChallenge/SessionLogin` остаются каноническими потоками обычной авторизации;
|
||
- pairing через ESP идёт отдельными `op` и только подготавливает безопасное добавление новой сессии;
|
||
- решение об одобрении pairing принимает любая уже авторизованная доверенная сессия пользователя.
|
||
|
||
## 1. Поток авторизации
|
||
|
||
Поддерживаются два сценария:
|
||
|
||
1. Создание новой сессии:
|
||
`AuthChallenge` -> `CreateAuthSession`
|
||
2. Вход в существующую сессию:
|
||
`SessionChallenge` -> `SessionLogin`
|
||
|
||
`deviceKey` используется для создания новой сессии.
|
||
|
||
`sessionKey` используется для входа в уже созданную сессию.
|
||
|
||
`sessionKey` передаётся и хранится целиком одной строкой, например:
|
||
|
||
```text
|
||
ed25519/BASE64_PUBLIC_KEY
|
||
```
|
||
|
||
---
|
||
|
||
## 2. `AuthChallenge`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "AuthChallenge",
|
||
"requestId": "auth-001",
|
||
"payload": {
|
||
"login": "alice"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "AuthChallenge",
|
||
"requestId": "auth-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"authNonce": "8f2f0f71-0b1c-4ab2-8f5d-0bc5d6f6aa11"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `AuthChallenge`
|
||
|
||
- `400 / EMPTY_LOGIN` — пустой `login`.
|
||
- `400 / ALREADY_AUTHED` — по текущему соединению уже выполнена авторизация.
|
||
- `422 / UNKNOWN_USER` — пользователь с таким `login` не найден.
|
||
- `501 / SOLANA_IMPORT_FAILED` — сервер не смог проверить/импортировать пользователя из Solana при lazy-import.
|
||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера, если появится вне штатного сценария.
|
||
|
||
---
|
||
|
||
## 3. `CreateAuthSession`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "CreateAuthSession",
|
||
"requestId": "create-001",
|
||
"payload": {
|
||
"login": "alice",
|
||
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET",
|
||
"timeMs": 1774600000123,
|
||
"authNonce": "nonce",
|
||
"deviceKey": "BASE64_DEVICE_PUBLIC_KEY",
|
||
"signatureB64": "BASE64_SIGNATURE",
|
||
"sessionType": 1,
|
||
"clientPlatform": "Web",
|
||
"clientInfo": "Android 15; Pixel 9"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Строка для подписи
|
||
|
||
```text
|
||
AUTH_CREATE_SESSION:{login}:{sessionKey}:{storagePwd}:{timeMs}:{authNonce}
|
||
```
|
||
|
||
### Дополнительная проверка ключа
|
||
|
||
Перед проверкой подписи сервер должен:
|
||
|
||
1. взять актуальный `solana_users.device_key`;
|
||
2. сравнить его с `payload.deviceKey`;
|
||
3. только потом проверять подпись.
|
||
|
||
Если ключ не совпадает, сервер возвращает ошибку `DEVICE_KEY_NOT_ACTUAL`.
|
||
|
||
На будущее:
|
||
|
||
- для ротации `device_key` желательно добавить перепроверку через Solana.
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "CreateAuthSession",
|
||
"requestId": "create-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"sessionId": "sess_7c5e5c4b"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `CreateAuthSession`
|
||
|
||
- `400 / NO_STEP1_CONTEXT` — для данного соединения не был корректно выполнен `AuthChallenge`.
|
||
- `400 / EMPTY_LOGIN` — пустой `login`.
|
||
- `400 / LOGIN_MISMATCH` — `login` не совпадает с тем, для кого был выдан `authNonce`.
|
||
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при повторном чтении пользователя.
|
||
- `422 / USER_NOT_FOUND` — пользователь не найден.
|
||
- `501 / NO_LOGIN` — у пользователя на сервере не заполнен `login`.
|
||
- `400 / EMPTY_STORAGE_PWD` — пустой `storagePwd`.
|
||
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
|
||
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` или `deviceKey` не поддерживается текущим сервером.
|
||
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey`, `deviceKey` или `signatureB64`.
|
||
- `400 / EMPTY_SIGNATURE` — пустая подпись.
|
||
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
|
||
- `400 / NO_DEVICE_KEY` — у пользователя в БД отсутствует `deviceKey`.
|
||
- `400 / EMPTY_AUTH_NONCE` — пустой `authNonce`.
|
||
- `400 / AUTH_NONCE_MISMATCH` — `authNonce` не соответствует значению из `AuthChallenge`.
|
||
- `400 / EMPTY_DEVICE_KEY` — в запросе не передан `deviceKey`.
|
||
- `422 / DEVICE_KEY_NOT_ACTUAL` — `deviceKey` не совпадает с актуальной версией на сервере.
|
||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||
- `501 / DB_ERROR_SESSION_CREATE` — ошибка БД при создании записи активной сессии.
|
||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||
|
||
---
|
||
|
||
## 4. `SessionChallenge`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "SessionChallenge",
|
||
"requestId": "sch-001",
|
||
"payload": {
|
||
"sessionId": "sess_7c5e5c4b"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "SessionChallenge",
|
||
"requestId": "sch-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"nonce": "0e5bb0f4-c7d8-4efb-b44d-bf31a6126c66"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `SessionChallenge`
|
||
|
||
- `400 / EMPTY_SESSION_ID` — пустой `sessionId`.
|
||
- `501 / DB_ERROR` — ошибка БД при чтении сессии.
|
||
- `422 / SESSION_NOT_FOUND` — сессия не найдена.
|
||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||
|
||
---
|
||
|
||
## 5. `SessionLogin`
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "SessionLogin",
|
||
"requestId": "slogin-001",
|
||
"payload": {
|
||
"sessionId": "sess_7c5e5c4b",
|
||
"sessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||
"timeMs": 1774600010456,
|
||
"signatureB64": "BASE64_SIGNATURE",
|
||
"sessionType": 1,
|
||
"clientPlatform": "Web",
|
||
"clientInfo": "Android 15; Pixel 9"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Строка для подписи
|
||
|
||
```text
|
||
SESSION_LOGIN:{sessionId}:{timeMs}:{nonce}
|
||
```
|
||
|
||
### Дополнительная проверка ключа
|
||
|
||
Перед проверкой подписи сервер должен:
|
||
|
||
1. взять `active_sessions.session_key`;
|
||
2. сравнить его с `payload.sessionKey`;
|
||
3. только потом проверять подпись.
|
||
|
||
Если ключ не совпадает, сервер возвращает ошибку `SESSION_KEY_NOT_ACTUAL`.
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "SessionLogin",
|
||
"requestId": "slogin-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"storagePwd": "BASE64_OR_APP_SPECIFIC_SECRET"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Специфические коды ошибок `SessionLogin`
|
||
|
||
- `400 / EMPTY_SESSION_ID` — пустой `sessionId`.
|
||
- `400 / NO_CHALLENGE` — перед `SessionLogin` не был успешно выполнен `SessionChallenge` либо nonce уже истёк.
|
||
- `400 / SESSION_ID_MISMATCH` — nonce был выдан для другого `sessionId`.
|
||
- `400 / TIME_SKEW` — время клиента отличается от серверного больше допустимого окна.
|
||
- `400 / EMPTY_SIGNATURE` — пустая подпись.
|
||
- `400 / EMPTY_SESSION_KEY` — пустой `sessionKey`.
|
||
- `501 / DB_ERROR` — ошибка БД при чтении сессии.
|
||
- `422 / SESSION_NOT_FOUND` — сессия не найдена.
|
||
- `501 / NO_SESSION_KEY` — у сессии отсутствует `session_key`.
|
||
- `422 / SESSION_KEY_NOT_ACTUAL` — переданный `sessionKey` не совпадает с актуальной версией на сервере.
|
||
- `422 / UNSUPPORTED_KEY_ALGORITHM` — префикс алгоритма в `sessionKey` не поддерживается текущим сервером.
|
||
- `400 / BAD_BASE64` — неверный Base64 в `sessionKey` или `signatureB64`.
|
||
- `422 / BAD_SIGNATURE` — подпись не прошла проверку.
|
||
- `460 / SESSION_TYPE_MISMATCH` — `sessionType` не совпадает с типом сессии, уже опубликованным для этого `sessionKey` в Solana PDA.
|
||
- `501 / SESSION_TYPE_PDA_CHECK_FAILED` — сервер не смог проверить `sessionType` по Solana PDA.
|
||
- `501 / DB_ERROR_USER_LOOKUP` — ошибка БД при чтении пользователя для этой сессии.
|
||
- `422 / USER_NOT_FOUND_FOR_SESSION` — пользователь, которому принадлежит сессия, не найден.
|
||
- `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера.
|
||
|
||
---
|
||
|
||
## 6. Pairing через homeserver/ESP
|
||
|
||
Новые `op`, относящиеся к этому сценарию:
|
||
|
||
- `UpsertEspPairingSettings`
|
||
- `StartEspPairing`
|
||
- `ListEspPairingRequests`
|
||
- `ApproveEspPairing`
|
||
- `RejectEspPairing`
|
||
- `GetEspPairingStatus`
|
||
|
||
В этом потоке:
|
||
|
||
- новое устройство не владеет `deviceKey` и не проходит обычный `CreateAuthSession`;
|
||
- пароль проверяется сервером только как фильтр;
|
||
- решение об одобрении принимает уже авторизованная доверенная сессия пользователя;
|
||
- сервер не расшифровывает `encryptedPayload` и не становится источником приватных ключей.
|
||
|
||
Точные форматы этих операций см. в `03_Session_Management_API.md` и в протокольном документе:
|
||
|
||
- `Dev_Docs/Протоколы/ESP_Pairing_и_режимы_подключения.md`
|
||
|
||
---
|
||
|
||
## 6. Пример ошибки
|
||
|
||
```json
|
||
{
|
||
"op": "SessionLogin",
|
||
"requestId": "slogin-001",
|
||
"status": 403,
|
||
"ok": false,
|
||
"error": "SESSION_KEY_NOT_ACTUAL",
|
||
"message": "session_key не соответствует актуальной версии",
|
||
"payload": {
|
||
}
|
||
}
|
||
```
|