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

> OAuth 2.0, привязка токена к клиенту по DPoP или mTLS, серверный nonce, scopes.

## OAuth 2.0

API LegalStack использует OAuth 2.0 с требованиями OAuth 2.1: PKCE обязателен, токены короткоживущие, каждый access token привязан к клиенту. Точка выдачи токенов — `POST https://api.legalstack.ru/oauth/token`, метаданные — `https://api.legalstack.ru/.well-known/oauth-authorization-server`.

Поддерживаемые сценарии получения токена:

- **Client Credentials** — для серверных интеграций от имени команды.
- **Authorization Code с PKCE** — для приложений, действующих от имени пользователя.

Все операции OAuth (авторизация, обмен кода, обновление, отзыв и проверка токена) — в разделе [«OAuth»](https://docs.legalstack.ru/api/security/oauth) справочника API.

## Привязка токена к клиенту

Платформа не выдаёт токенов, которыми может воспользоваться любой, кто их перехватил. Каждый access token привязан к клиенту одним из двух способов, и ресурсные сервисы отклоняют токен без привязки:

- **DPoP** (RFC 9449) — токен привязан к ключу, который клиент сгенерировал сам и хранит у себя. Это основной способ для интеграций, SDK и MCP-клиентов. В справочнике API схема безопасности называется ``dpop``.
- **mTLS** (RFC 8705) — токен привязан к клиентскому сертификату, которым установлено TLS-соединение. Так работают сервисы самой платформы и клиенты, которым платформа выдала сертификат. Схема безопасности — ``mutualTLS``.

### Как работает DPoP

1. Клиент один раз создаёт пару ключей. Поддерживаемые алгоритмы: `ES256, ES384, ES512, EdDSA, PS256, PS384, PS512, RS256`. Приватный ключ не покидает клиента.
2. На каждый HTTP-запрос клиент подписывает этим ключом короткий JWT-доказательство и передаёт его в заголовке `DPoP`. В доказательстве: `htm` (метод), `htu` (URL без query и fragment), `iat`, уникальный `jti`, `nonce` сервера, а для запросов с токеном ещё `ath` — SHA-256 от access token в base64url. В заголовке JWT: `typ: dpop+jwt`, `alg` и публичный ключ в `jwk`.
3. Токен передаётся со схемой `DPoP`, не `Bearer`:

Запрос:

```http
GET /api/v1/tasks HTTP/1.1
Authorization: DPoP <access_token>
DPoP: <доказательство для GET https://api.legalstack.ru/api/v1/tasks>
```

4. Сервер проверяет подпись, совпадение ключа с привязкой в токене (`cnf.jkt`), метод, адрес, свежесть, `ath`, nonce и однократность `jti`. Доказательство действительно `300` секунд с момента `iat`, повторное использование `jti` отклоняется.

### Серверный nonce

Чтобы доказательства нельзя было заготовить впрок, сервер требует включать в них свой nonce и присылает актуальное значение в заголовке `DPoP-Nonce` каждого ответа. Значение действует не меньше `120` секунд с момента выдачи, поэтому для следующего запроса берите nonce из последнего ответа.

Если клиент только запускается или простаивал дольше этого срока, получите nonce заранее: `GET` на путь `/.well-known/dpop-nonce` того же хоста API отвечает `204` без тела и с заголовком `DPoP-Nonce`. Токен и доказательство для этого запроса не нужны. Параллельные запросы после простоя дождитесь одного такого запроса, тогда все они уйдут с действующим nonce.

Запрос без nonce или с устаревшим значением не выполняется:

- точка выдачи токенов отвечает `400` с `error: use_dpop_nonce`;
- ресурсные сервисы отвечают `401` с кодом `USE_DPOP_NONCE` и заголовком `WWW-Authenticate: DPoP error="use_dpop_nonce"`.

В обоих случаях в ответе есть `DPoP-Nonce`: повторите тот же запрос с новым доказательством, включив это значение в `nonce`.

### Получение токена

На `POST /oauth/token` клиент передаёт заголовок `DPoP` с доказательством для этого запроса. В ответе `token_type` равен `DPoP`, а токен содержит отпечаток ключа в `cnf.jkt`. Запрос за токеном без доказательства и без клиентского сертификата отклоняется с `error: invalid_dpop_proof`.

Ответ:

```http
HTTP/1.1 200 OK
DPoP-Nonce: <nonce для следующего доказательства>
Content-Type: application/json

{ "access_token": "…", "token_type": "DPoP", "expires_in": 900, "scope": "tasks:read" }
```

Токены короткоживущие; обновляйте их по `refresh_token` или повторным client-credentials запросом тем же ключом DPoP. Ответ `401` с кодом `TOKEN_EXPIRED` — сигнал получить новый токен. Refresh token выдаётся только клиентам с DPoP и клиентам с сертификатом, при каждом обновлении он ротируется.

## Ресурсы

Каждый токен выдаётся для одного ресурса платформы, и ресурс указывается параметром `resource` (RFC 8707) при авторизации и при запросе токена:

| Ресурс | Для чего | Scope |
| --- | --- | --- |
| `https://api.legalstack.ru` | REST API, ресурс по умолчанию | все scope каталога, кроме `mcp:tools:execute` |
| `https://mcp.legalstack.ru/mcp` | MCP-сервер, все разделы | `mcp:tools:execute` |
| `https://mcp.legalstack.ru/mcp/<раздел>` | MCP-сервер, один раздел | `mcp:tools:execute` |

Запрос токена для любого другого адреса отклоняется с `error: invalid_target`. В настройках OAuth-приложения можно сузить список ресурсов, для которых приложение получает токены: пустой список разрешает все ресурсы платформы.

## Персональные ключи [#personal-keys]

Персональный ключ создаётся в «Настройки → API → API-ключи» и начинается с `lk_`. Это долгоживущий секрет пользователя, и в запросах к API он не передаётся: ресурсные сервисы отвечают `401` с кодом `INVALID_TOKEN` на любой запрос с ключом в заголовке `Authorization`. Ключ обменивается на короткоживущий токен доступа по RFC 8693, а токен привязывается к ключу DPoP так же, как в разделе выше.

Обмен ключа:

```http
POST /oauth/token HTTP/1.1
Host: api.legalstack.ru
Content-Type: application/x-www-form-urlencoded
DPoP: <доказательство для POST https://api.legalstack.ru/oauth/token>

grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=lk_…
&subject_token_type=urn:legalstack:params:oauth:token-type:personal-token
&resource=https://api.legalstack.ru
&scope=tasks:read
```

Ответ:

```json
{
  "access_token": "…",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "DPoP",
  "expires_in": 900,
  "scope": "tasks:read"
}
```

- **Без DPoP ключ не обменивается.** Запрос без доказательства получает `error: invalid_dpop_proof`, первый запрос с доказательством без nonce получает `use_dpop_nonce` и значение в заголовке `DPoP-Nonce`.
- **Scope токена не шире ключа и ресурса.** Без параметра `scope` токен получает все scope ключа, доступные для ресурса. Ключ без ограничений получает все scope ресурса. Запрос scope, которого нет у ключа или у ресурса, отклоняется с `error: invalid_scope`.
- **Токен не переживает ключ.** Срок токена не больше срока ключа, а отзыв ключа сразу делает недействительными все выданные по нему токены.
- **Ключ не управляет учётными данными.** Создавать и отзывать ключи и OAuth-приложения можно только из пользовательской сессии в веб-приложении.

## Scopes

Доступ контролируется scopes вида `<домен>:<действие>` — например `tasks:read`, `registry:write`, `webhooks:write`. Требуемые scopes каждой операции указаны в [справочнике API](https://docs.legalstack.ru/api), полный список — в [каталоге scope](https://docs.legalstack.ru/api/security/scopes). Запрашивайте минимально необходимый набор: запрос с недостающим scope получит `403` с кодом `OAUTH_INSUFFICIENT_SCOPE`.

## Ключи подписи

Публичные ключи для проверки JWT публикуются в JWKS: `https://api.legalstack.ru/.well-known/jwks.json`. Ключи ротируются — резолвьте их по `kid` из заголовка токена, а не кэшируйте единственный ключ навсегда.
