# Авторизация и вход

Документ описывает администрирование домена auth: провайдеры и сервисы аутентификации, политики входа, синхронизацию каталогов, настройку типов входа и регистрации, а также справочник ключей appsettings.json. Для администраторов и инженеров поддержки.

## Обзор и механизмы администрирования

Администрирование `auth` построено вокруг связки "сервис аутентификации -> провайдер аутентификации -> политики входа". Основной UI-контур расположен в разделе `system -> подключения`.

В домене используются все три уровня:

- формы `dbadmin` (провайдеры и сервисы);
- отдельная страница AdminSPA для внутреннего провайдера, `EntityEditor` — для внешних;
- служебные Admin API для проверки LDAP и получения конфигураций.


Настройки домена доступны через формы автоадминки, EntityEditor и служебные API.


Формы автоадминки для провайдеров, сервисов и профилей синхронизации:

| Alias формы | Название | Таблица БД | Что настраивается |
|-------------|----------|------------|-------------------|
| `authentication-providers` | Провайдеры аутентификации | `dbo.AuthenticationProviders` | Тип провайдера, активность, 2FA, видимость |
| `authentication-provider-groups` | Доступ к провайдеру по группам | `dbo.AuthenticationProviderGroups` | Ограничения входа по группам |
| `authentication-provider-domains` | Домены провайдера аутентификации | `dbo.AuthenticationProviderDomains` | Ограничение отображения провайдера по доменам (FQDN) |
| `active-directory-service-settings` | Сервис Active Directory | `dbo.LDAPServicesCredentials` | Подключение к AD |
| `openldap-service-settings` | Сервис OpenLDAP | `dbo.OpenLDAPServicesCredentials` | Подключение к OpenLDAP |
| `oauth-service-settings` | Сервис OAuth | `dbo.OAuthServicesSettings` | OIDC/OAuth параметры |
| `saml-service-settings` | Сервис SAML | `dbo.SAMLServicesSettings` | IdP metadata, сертификаты, claims |
| `radius-service-settings` | Сервис Radius | `dbo.RadiusServicesCredentials` | Radius-сервер |
| `multifactor-service-settings` | Сервис Multifactor | `dbo.MultifactorCredentials` | Внешний второй фактор |
| `synchronizations` | Синхронизации (AD, LDAP) | `dbo.SynchronizationProfiles` | Профили sync с каталогами |
| `synchronization-settings` | Настройки синхронизации | `dbo.SynchronizationProfilesADSettings` | Параметры AD-синхронизации |


В разделе **system → подключения** при создании или редактировании сервиса аутентификации форма отображает поля, специфичные для выбранного типа сервиса. Обязательные поля помечены звёздочкой (*). Кнопка «Сохранить» становится активной только после заполнения всех обязательных полей текущего типа.

Состав обязательных полей зависит от типа сервиса:

| Тип сервиса | Обязательные поля |
|-------------|-------------------|
| Active Directory | Домен, Логин для доступа к AD, Пароль для доступа к AD |
| Radius | IP Radius сервера, Порт Radius сервера, Shared secret |
| PayControl | System ID, API URL, Server Signer, User ID, Reg Accepted Ext Sys Name, Allow Update Ext Sys Name |
| TranslateService | Login, Password, Key, URL, Region |
| OpenLDAP | Server Address |
| DSS CryptoPro | Sign Server, STS App Name, Host, Client ID |

Для типов OAuth, SAML и некоторых других, не имеющих обязательных полей в конфигурации, кнопка «Сохранить» активна сразу после выбора типа сервиса.

### EntityEditor, Admin API и Runtime API

Расширенное редактирование провайдеров по JSON-схеме:

| Схема JSON | Таблица | Назначение |
|-----------|---------|------------|
| `authenticationProviders` | `dbo.AuthenticationProviders` | Расширенное редактирование провайдеров |


Служебные маршруты, используемые админ-интерфейсом:

| Контроллер | Маршрут | Методы | Назначение |
|-----------|---------|--------|------------|
| `AuthenticationProvidersController` | `/api/admin/authentication-providers` | GET, POST, PUT, DELETE | Список, карточка и метаданные, создание, изменение и удаление провайдеров |
| `LdapController` | `/api/admin/ldap` | GET, POST | Проверка провайдеров, поиск пользователей и групп в LDAP |

**Чтение провайдеров через административный API.** Старый бесфильтровый `GET /api/admin/authentication-providers` возвращает краткий список активных провайдеров (`AuthenticationProviderShortDto`) и не меняется. Для админ-формы у контроллера три отдельных GET; все ответы обёрнуты в `ApiResultDto<T>`, без доступа к админ-API — `403`:

- `GET /api/admin/authentication-providers/list` → `List<AuthenticationProviderAdminDto>` — полный список (активные и неактивные). Если строки встроенного входа в БД нет, список первым элементом получает виртуальную запись None с `id = 0` (без INSERT). Порядок: провайдер по умолчанию, затем None, затем по `Id`.
- `GET /api/admin/authentication-providers/metadata` → `AuthenticationProviderAdminMetadataDto` — справочники формы: `ProviderTypes` (типы провайдера), `Services` (сервисы типов ActiveDirectory / Radius / OpenLDAP / SAML / OAuth), `Groups`, `SecondFactorTypes`, `SecondFactorServices` (сервисы Multifactor). Элементы `Services` / `Groups` / `SecondFactorServices` — `{ Id, Guid, Name }`.
- `GET /api/admin/authentication-providers/{id}` → `AuthenticationProviderAdminDto` — карточка. Для `id = 0` без строки в БД возвращается виртуальная запись None (без INSERT); неизвестный реальный `id` — провайдер не найден.

`AuthenticationProviderAdminDto`: `Id`, `ProviderType`, `Description`, `ServiceId` / `ServiceGuid` / `ServiceDescription`, `IsActive`, `IsDefault`, `IsHidden`, `LinkForPasswordRecovery`, `SecondFactorType` / `SecondFactorServiceId`, `BotName`, `BotId`, `Groups` (GUID групп) и флаг `HasBotToken`. Секреты не раскрываются: токен Telegram-бота в ответах не возвращается — только признак `HasBotToken`.

**Запись провайдеров через административный API.** `POST` создаёт провайдера, `Id` из тела игнорируется; `PUT /{id}` и `DELETE /{id}` берут идентификатор только из маршрута и отвечают `404`, если провайдера нет. Встроенный вход отдаётся в списке с `id = 0` и тогда, когда отдельной строки в таблице нет: `PUT` для `id = 0` создаёт эту строку, а `DELETE` для `id = 0` запрещён. В `PUT` незаполненные `ServiceId`, `SecondFactorType`, `BotName` и пустой `BotId` оставляют прежние значения, а явный `SecondFactorType = None` очищает поля второго фактора и бота. Доменные ограничения провайдера и настройки сервиса этот контракт не меняет.

**Журналирование операций записи.** Каждая успешная операция записи через административный API — создание, изменение, удаление — оставляет ровно одну запись в журнале действий администраторов с инициатором, идентификатором и типом провайдера; отклонённые операции записей не оставляют. Формат записей и границы, в том числе то, что путь через универсальный редактор сущностей не журналируется, — в разделе [«Журналирование входов и изменений провайдеров»](https://help.1forma.ru/domains/auth/admin.md#журналирование-входов-и-изменений-провайдеров).

Проверки при записи общие с формой `EntityEditor`: оба пути сохраняют и удаляют провайдеров через `AuthenticationProvidersConfigurationService`, проверка идёт в одной сериализуемой транзакции с записью, а нарушение возвращается как `400` с текстом причины. Тип существующего провайдера не меняется; внешнему провайдеру нужен существующий сервис того же типа, не занятый другим провайдером; внутренний провайдер один и без сервиса; провайдер по умолчанию один; несуществующая группа отклоняется. Сохранение или удаление отклоняется и тогда, когда после него не останется активных провайдеров, а строка встроенного входа останется в таблице и скроет виртуальный встроенный вход; удалить саму строку встроенного входа можно всегда — вход вернётся виртуальной записью.

**Обязательные поля второго фактора.** Для второго фактора Telegram нужен непустой идентификатор бота (`BotId`), для второго фактора типа «сервис» — сервис второго фактора (`SecondFactorServiceId`). Иначе запись отклоняется с `400` и текстом «Для второго фактора Telegram необходимо указать идентификатор бота» или «Для второго фактора типа «сервис» необходимо указать сервис второго фактора». Проверяются итоговые значения: в `PUT` — после подстановки прежних, в форме `EntityEditor` — вместе с сохранёнными полями второго фактора. Поэтому пустой `BotId` при уже сохранённом токене бота запись не отклоняет, а провайдер с неполными полями второго фактора не сохраняется и при правке других полей, пока эти поля не заполнены.

**Слияние значений при сохранении.** Правило одно для административного API и для формы расширенного редактирования. Поля второго фактора и бота, которых нет в запросе, берутся из сохранённой записи, поэтому сохранение формы, где эти поля не показаны, их не стирает. Очистка выполняется только явным указанием второго фактора «нет»: тогда обнуляются и сервис второго фактора, и идентификатор с именем бота. Пустой токен бота — пустая строка или пробелы — трактуется как «оставить прежний». Пустой список групп означает очистку доменных связей, а не «не трогать». Тип существующего провайдера сохранением не меняется. Токен бота не возвращается ни при чтении, ни в списке, ни в ответах на создание и обновление: во всех четырёх случаях отдаётся только признак `HasBotToken`.

### Кастомная страница провайдеров в AdminSPA

Внутренний (встроенный) провайдер аутентификации настраивается на отдельной странице, а не в универсальном редакторе сущности. Пункт меню «Провайдеры аутентификации» ведёт на список по адресу `/administration/authentication-providers`, форма внутреннего провайдера открывается по `/administration/authentication-providers/internal`. Оба маршрута закрыты админским guard (`AuthAdminGuard`).

В списке видны все провайдеры: идентификатор, тип, описание, сервис, признак «Включён», тип второго фактора и признак наличия токена бота. Подсказка над списком поясняет разделение: внутренний провайдер настраивается на отдельной странице, внешние — в стандартном редакторе. На форму внутреннего провайдера ведёт кнопка «Настроить внутренний провайдер», на прежний универсальный редактор — кнопка «Стандартный редактор».

Форма внутреннего провайдера содержит описание, признаки «Включён» и «Скрыт на форме входа», ссылку для восстановления пароля и блок «Второй фактор»: тип второго фактора, сервис второго фактора, имя Telegram-бота и токен Telegram-бота. Пока записи внутреннего провайдера в базе нет, список отдаёт её виртуальной строкой с нулевым идентификатором; сохранение формы создаёт реальную запись, после сохранения карточка перечитывается с сервера.

Токен бота наружу не отдаётся: форма показывает только признак «задан» или «не задан» и поле для нового значения — пустое поле оставляет прежний токен. Кнопка «Очистить» спрашивает подтверждение («Очистка токена отключит второй фактор этого провайдера») и вместе с токеном переводит тип второго фактора в «Не используется».


Маршруты входа, по которым проверяют, что настройки работают:

| Контроллер | Маршрут | Что проверяет |
|-----------|---------|---------------|
| `AuthController` | `/api/auth/token-v2`, `/api/auth/token/refresh`, `/api/auth/info` | Базовый password/refresh контур |
| `SamlAuthController` | `/api/auth/saml/*` | SAML SSO-поток |
| `OAuthController` | `/api/auth/oauth` | OAuth/OIDC вход и выход |

### Журналирование входов и изменений провайдеров

Каждый успешный вход записывается в `dbo.LoginsLog` отдельной строкой: пользователь, адрес, дата, адрес запроса без параметров, клиент (`UserAgent`), имя компьютера, протокол аутентификации и дополнительные сведения (провайдер, заголовок `X-Real-IP`, пояснение). Повторный вход с того же адреса через `token-v2` и параллельные входы одного пользователя дают каждый свою строку.

Дедупликация сохранена для путей, которые аутентифицируют клиента в каждом запросе, и для продления маркера: вход Windows/Negotiate и Basic с того же адреса в пределах окна активности (`UpdateLastActiveTime`, по умолчанию 5 минут) отдельной записи не создаёт, а сравнение идёт с последним входом того же вида — поэтому первый доменный вход после входа паролем с того же адреса всё равно пишется. Сравнение продления маркера (`LogRefreshTokenRequests`) ведётся с последним входом любого вида, как и раньше.

Создание, изменение и удаление провайдера аутентификации оставляют запись в журнале действий администраторов (`dbo.ActionLog`). Запись появляется только после успешной операции, выполненной через административный API, и содержит инициатора (пользователя сеанса), идентификатор и тип провайдера.

| Операция | Маршрут | Тип записи в журнале |
|---|---|---|
| Создание | `POST /api/admin/authentication-providers` | `authentication_provider_create` |
| Изменение | `PUT /api/admin/authentication-providers/{id}` | `authentication_provider_update`, а для записи встроенного входа (`id = 0`) — `authentication_provider_create` |
| Удаление | `DELETE /api/admin/authentication-providers/{id}` | `authentication_provider_delete` |

Отклонённые операции — пустое тело запроса, неизвестный идентификатор, запрет удаления записи встроенного входа — записей в журнале не оставляют. Создание, изменение и удаление провайдера через универсальный редактор сущностей (схема `authenticationProviders`) в журнал не пишутся. Значения секретов в запись не попадают: токен бота наружу и в журнал не отдаётся, сохраняется только признак `HasBotToken`.

## Ключевые настройки

### 1. Провайдеры аутентификации

**Где настраивается:** `authentication-providers`, `authentication-provider-groups`, `authentication-provider-domains`

**Таблицы БД:**

- `dbo.AuthenticationProviders`
- `dbo.AuthenticationProviderGroups`
- `dbo.AuthenticationProviderDomains`

**Что контролируется:**

- тип провайдера (`ActiveDirectory`, `OpenLDAP`, `OAuth`, `SAML`, `Radius`, а также `None` — встроенный вход по логину и паролю);
- флаги активности/скрытия/"по умолчанию";
- второй фактор;
- доступность провайдера для конкретных групп;
- привязка провайдера к доменам (FQDN) — на странице авторизации провайдер показывается только при входе с указанных доменов. Если список доменов пуст — провайдер доступен со всех доменов. Список настроенных доменов передаётся на фронт через `app-settings.json → Providers`, фильтрация выполняется на стороне клиентского приложения по сравнению текущего `hostname` страницы с заданным списком.

### 2. Сервисы внешней аутентификации

#### OpenID Connect / OAuth: формирование `scope` из настроек провайдера

Для OIDC-провайдера список `scope` в запросе к Identity Provider теперь может задаваться явно и/или вычисляться по конфигурации провайдера.

Что важно при настройке:

- `openid` должен присутствовать всегда — это базовый scope протокола OpenID Connect;
- если в `SettingsJson` задан ключ `Scopes`, платформа использует его как явный список дополнительных scope;
- если `Scopes` не задан, платформа вычисляет дополнительные scope по списку `Claims`;
- для claim `phone_number` и `phone_number_verified` требуется scope `phone`;
- для claim `email` и `email_verified` требуется scope `email`;
- для claim группы профиля (`name`, `family_name`, `given_name`, `middle_name`, `nickname`, `preferred_username`, `profile`, `picture`, `website`, `gender`, `birthdate`, `zoneinfo`, `locale`, `updated_at`) требуется scope `profile`;
- для claim `address` требуется scope `address`.

Это изменение нужно для сценариев, где пользователь сопоставляется не по стандартному `sub`, а по другому claim, например по номеру телефона. Ранее запрос мог уходить только с `scope=openid`, из-за чего IdP не возвращал нужный claim и вход завершался ошибкой.

Пример настройки входа по телефону:

```json
{
  "Claims": ["phone_number"],
  "Scopes": ["phone"],
  "ClaimsMapperConfig": {
    "MapperType": "ByAttribute",
    "IdentityClaim": "phone_number",
    "UserAttribute": "MobilePhone"
  }
}
```

Практическое правило: если используемый для идентификации claim зависит от стандартного OIDC scope, лучше явно указать `Scopes` в `SettingsJson`, чтобы поведение не зависело от неявного вывода.

#### Сервис без блока настроек

Блок настроек сервиса (в форме — «Settings (JSON)») заполнять не обязательно: сервис внешней аутентификации сохраняется и с пустым блоком. Пустое значение записывается как пустой объект `{}`, поэтому сохранение сервиса без настроек не завершается ошибкой, а сервис создаётся штатно.

Что из этого следует:

- **Настройки по умолчанию.** Сервис, сохранённый без блока настроек, получает значения по умолчанию. Для «Режима продления сессии» это «Через IdP» — то же значение, которое указано по умолчанию у ключа `RenewalMode` в разделе «SAML (ADFS)».
- **Сохранение заменяет настройки целиком.** Если сервис сохранить повторно без блока настроек, прежние значения (`AuthorityUrl`, `CallbackPath`, `Claims`, `ClaimsMapperConfig` у OAuth/OIDC, `RenewalMode` у SAML) заменяются на пустой объект и возвращаются к значениям по умолчанию. Форма админки отправляет настройки вместе с остальными полями, поэтому при работе через форму значения не теряются.

**Где настраивается:** формы `*-service-settings` в `system -> подключения`.

**Таблицы БД:**

- `dbo.LDAPServicesCredentials`
- `dbo.OpenLDAPServicesCredentials`
- `dbo.OAuthServicesSettings`
- `dbo.SAMLServicesSettings`
- `dbo.RadiusServicesCredentials`
- `dbo.MultifactorCredentials`

**Что контролируется:** точки API (endpoint), учётные данные, сертификаты, сопоставление claims и технические параметры согласования.

### 3. Политики входа и защитные лимиты

Политики входа и защитные лимиты настраиваются в общих системных настройках (`system-settings`) и пользовательских ключах (`sys_user`) и определяют правила безопасности аутентификации.

**Что контролируется:**

- лимиты неудачных попыток входа и captcha;
- ограничения на логин по email (ключ `ForbidEmailAsLogin`): если включён — система запрещает вход по полю Email; разрешён только вход по Логину/Нику. Используется, когда email-адреса не уникальны или не предназначены для аутентификации;
- правила регистрации и password-политики.

#### Защита от брутфорса (общие настройки → блок аутентификации)

Три независимых счётчика ограничивают перебор паролей:

| Настройка | Эффект при превышении |
|-----------|----------------------|
| **Максимальное число попыток логина до блокировки** | Учётная запись блокируется. Разблокировка — только администратором |
| **Максимальное число попыток логина пользователя до капчи** | Запрашивается ввод captcha при следующих попытках |
| **Максимальное число попыток логина с IP до капчи** | IP-адрес блокируется; для разблокировки выводится captcha |

⚠️ Все три счётчика работают независимо. Если значение пустое — соответствующая защита выключена.

**Управление блокировками по IP.** Заблокированные по IP входы видны в форме автоадминки «Блокировка входа по IP» (раздел форм, папка «Прочее»): список адресов с числом неудачных попыток по каждому. Снять блокировку адреса — обнулить счётчик попыток правкой строки или удалить строку. Кнопка «Очистить» снимает блокировки сразу со всех адресов: список блокировок очищается целиком, форма обновляется. Операция доступна только суперадминистратору и записывается в журнал действий с числом удалённых записей.

**Работа за балансировщиком нагрузки.** Если 1Форма стоит за reverse proxy / load balancer, реальные IP клиентов нужно прокидывать через заголовки `X-Real-IP` и `X-Forwarded-Proto`. В `appsettings.json` за это отвечает секция `ForwardHeaders` — без её настройки счётчик попыток «с IP» будет видеть IP балансировщика как единственный источник и блокировать всех пользователей сразу.

**Адрес страницы логина для редиректа после API-ошибок** (с v2.257) задаётся ключом `AuthTokenLoginUrl` в `appsettings.json`. Используется в сценарии `~/api/...?auth=true` (см. [`user-ui/admin.md`](https://help.1forma.ru/domains/user-ui/admin.md), §9.2).

**Доступ по домену и `localhost`.** Если пользователю ограничен список доменов, с которых разрешён вход, проверка выполняется и для запросов с `Host: localhost`. Обход проверки для `localhost` включается ключом `AllowLocalhostDomainAccessBypass` в `appsettings.json`; по умолчанию он выключен, а при включении вне режима разработки приложение не запускается — конфигурация отвергается при чтении. Ключ действует одинаково на вход по логину и на перевоплощение.

Правило одно на все способы входа: тот же доменный контроль стоит перед выпуском токена при входе по коду и восстановлении пароля, на продлении токена, при SAML и OIDC, после подтверждения внешним MFA-провайдером, при Windows SSO и на мобильных входах — по номеру телефона и PIN, по ApiKey и по UserId. Сервисные учётки, которым домен не проверяется, перечисляются ключом `DomainAccessBypassLogins` в `appsettings.json`: сверка идёт по логину учётной записи без учёта регистра. Аварийный ключ `IgnoreDomainCheck` выключает доменную проверку целиком, а перевоплощение проверяется отдельным ключом `CheckImpersonationDomainAccess`. При отказе пользователь видит причину на странице входа, а с мобильного клиента не выдаются ни постоянный ключ, ни токен.

#### Политика паролей (для встроенных учётных записей)

**Область применения:** настройки учитываются при создании пользователей из режима администрирования и при сбросе пароля. Для **самостоятельной регистрации** действуют требования из пользовательского ключа `RegistrationFields` (см. ниже §«Самостоятельная регистрация»).

| Параметр | Поведение |
|----------|-----------|
| **Срок действия пароля (дни)** | Только forms-авторизация. Если задан — в профиле отображается счётчик дней до смены; при просрочке пользователь редиректится на форму смены при следующем входе |
| **Минимальная / Максимальная длина пароля** | Если не задано — не проверяется |
| **Содержит спецсимвол** (`! @ # $ %` и т. п.) | Флаг |
| **Содержит заглавную букву** | Флаг |
| **Пароль может совпадать с предыдущим** | Если выключено — нельзя устанавливать ранее использованный пароль. Применяется только при **смене пароля** существующего пользователя — у нового пользователя истории паролей нет, поэтому при создании (вручную в админке или через AD-синк с автогенерацией пароля) проверка не выполняется |
| **Пароль может совпадать с логином** | Если включено — допускается пароль, идентичный логину |
| **Длина истории паролей** | Сколько предыдущих паролей запоминается и блокируется к повторному использованию |
| **Проверять пароль на наличие даты рождения** | Запрет использовать дату рождения пользователя |
| **Минимальная длина последовательности символов для проверки** | Распознаются как небезопасные: подряд идущие цифры (`123`, `7890`), буквы по алфавиту (`abc`, `mno`), соседи на клавиатуре (рус.: `йцукен…`, `фывапрол…`, `ячсмить…`; англ.: `qwerty…`, `asdfgh…`, `zxcvbn…`) |
| **Минимальная длина повторяющегося паттерна для проверки** | Запрет паттернов вида `qweqwe`, `123123`. Срабатывает только при непосредственном повторе (не при разрозненном употреблении) |

**Повторяющийся паттерн требует максимальной длины.** Пароль сначала проверяется на длину, и только уложившийся в лимит разбирается на повторы. Поэтому проверка повторяющегося паттерна работает лишь при заданной максимальной длине пароля: если минимальная длина повторяющегося паттерна задана, а максимальная длина — нет, установка и смена пароля завершаются ошибкой конфигурации ещё до проверки самого пароля. Пароль длиннее максимума отклоняется сразу по длине — остальные требования к нему не проверяются и в ответе не перечисляются.

### Алгоритм хеширования паролей и срок действия кода восстановления

Для встроенных учётных записей платформа использует алгоритм хеширования паролей **Argon2id** (RFC 9106) как актуальный стандарт хранения паролей.

Текущие параметры хеширования:

- память — `19 MiB` (`m=19456`);
- итерации — `3`;
- parallelism — `1`;
- длина хеша — `32 байта`;
- длина salt — `16 байт`.

Хеш хранится в поле `PasswordHashPhc` в формате **PHC string**:
`$argon2id$v=19$m=19456,t=3,p=1$<salt-base64>$<hash-base64>`

Переход со старого формата выполнен без принудительного сброса паролей. При входе пользователя система сначала проверяет актуальный формат Argon2id. Если пароль был сохранён в старом формате Argon2i, выполняется резервная проверка по старому хешу, после чего при успешной аутентификации пароль автоматически перехешируется в формат Argon2id.

Это поведение относится только к встроенным учётным записям 1Формы. Для пользователей Active Directory, LDAP, SAML и OIDC пароль в БД 1Формы не хранится, поэтому поле `PasswordHashPhc` для них может оставаться пустым.


Параметр **«Срок действия кода для восстановления пароля в днях»** задаёт TTL email-кода, высылаемого по запросу «Забыли пароль?».

### 5. Personal Access Tokens (PAT)

> Реализовано (релиз 2.266)

**Где настраивается:**

- `appsettings.json` → секция `Auth` (серверные лимиты);
- спецправо `GENERATEPAT` на группу (управление через `system → подключения → спецправа` или dbadmin-форму `groups_spec`).

**Параметры `appsettings.json` → `Auth`:**

| Ключ | Тип | Дефолт | Описание |
|------|-----|--------|----------|
| `PatMaxTokensPerUser` | int | 10 | Максимум не-отозванных токенов на одного пользователя |
| `PatMaxExpirationDays` | int | 365 | Максимальный TTL токена в днях; `0` = бессрочные разрешены |

**Таблица БД:** `dbo.PersonalAccessTokens`

**Admin API:** `/api/admin/pat/*` — CRUD для администраторов (генерация, просмотр, отзыв токенов любого пользователя).

**User API:** `/api/user/pat/*` — генерация (только при наличии спецправа `GENERATEPAT`), просмотр и отзыв своих токенов.

**DevOps-задача:** при деплое/обновлении — убедиться, что ключи `PatMaxTokensPerUser` и `PatMaxExpirationDays` добавлены в `appsettings.json`.

### 6. appsettings.json — токены, сессии и LDAP

Серверные ключи секции `Auth` (и связанных) в `appsettings.json`:

| Ключ | По умолчанию | Назначение |
|------|---------|------------|
| `AuthTokenLoginUrl` | — | URL формы логина для редиректа из API при ошибке 401 (используется в сценарии `~/spa/entry/signin?fromUrl=...`, см. [`user-ui/admin.md` §9.2](https://help.1forma.ru/domains/user-ui/admin.md)). С v2.257+ |
| `AuthTokenExpiresInMinutes` | `1500` (25 ч) | TTL access-токена. ⚠️ В SPA по истечении нужна повторная авторизация |
| `AuthRefreshTokenExpiresInMinutes` | `43200` (30 дней) | TTL refresh-токена |
| `AuthTokenRefreshStrategy` | `SlidingExpiration` | Стратегия обновления: `SlidingExpiration` — авто-обновление при активности, `RefreshToken` — через `api/auth/token/refresh`, `None` — без обновлений |
| `AuthTokenRefreshPeriodInMinutes` | `20% от AuthTokenExpiresInMinutes` | Период автообновления при `SlidingExpiration`. Действует только при этой стратегии |
| `AuthTempTokenExpiresInMinutes` | — | TTL временного access-токена с ограниченными правами (выдаётся при смене пароля) |
| `AuthBasicAllowedPaths` | — | Регексы путей с разрешённой Basic-аутентификацией (через `;` или `,`). ⚠️ Экранируйте спецсимволы regex |
| `AuthLoginCodeExpiresInMinutes` | `5` | TTL кода регистрации/аутентификации, в минутах |
| `AuthRegistrationRateLimitPermitLimit` | `20` | Сколько запросов самостоятельной регистрации (`POST api/user`) принимается с одного IP за окно. Ноль или отрицательное значение снимает ограничение |
| `AuthRegistrationRateLimitWindowSeconds` | `600` | Длина окна ограничения регистраций по IP, в секундах. При превышении лимита возвращается `429 Too Many Requests` |
| `ConcurrentSessionMinLengthInMinutes` | `1` | Минимальная длительность активной сессии для конкурентной лицензии (через кэш `ConcurrentSessionsExpirationCache`). Меньше — быстрее перераспределение лицензий, больше — медленнее освобождение при закрытой вкладке |
| `LDAPSearchBase` | — | База поиска для LDAP |
| `UseSecureLDAP` | `true` | SSL для LDAP-подключений, включая провайдер OpenLDAP. При включении подключение идёт на порт 636 (LDAPS) |
| `LdapValidateServerCertificate` | `true` | Проверять ли сертификат LDAPS-сервера — цепочку доверия и имя узла. При `false` соединение принимается несмотря на ошибки политики TLS: площадки с самоподписанным или корпоративным центром сертификации, не доверенным операционной системе |
| `LdapTrustedCertificateThumbprints` | пусто | Список SHA-256 отпечатков доверенных сертификатов LDAP/AD. Пусто — дополнительная сверка не выполняется, непустой список — соединение принимается только при совпадении. Подробнее — под таблицей |
| `OidcAliases` | — | Алиасы OpenID-провайдеров (через запятую): `"sm,forma"` |
| `AlignedAuthResponseTimeMs` | `700` | Минимальное время ответа аутентификации (для защиты от time-attacks). При неуспехе — задержка до этого порога. Успешные ответы (200) — без задержки. `0` или отрицательное — отключение (для медленного AD) |

**Проверка сертификата LDAPS и сверка по отпечаткам.** Два ключа решают разные задачи и работают в паре: `LdapValidateServerCertificate` отвечает за обычную проверку цепочки доверия и имени узла, `LdapTrustedCertificateThumbprints` задаёт перечень конкретных сертификатов, которым разрешено подключение. Проверка цепочки выполняется первой, отпечатки сверяются после неё. Пустой список отключает только сверку отпечатков и не отменяет проверку цепочки. Настройка действует на три пути — вход через Active Directory, поиск и запрос статусов в каталоге, синхронизацию DirectoryInterop; в журнале они обозначены `AD.Authentication`, `AD.StatusSearch` и `DirectoryInterop.Sync`. Отдельного интерфейса, API или таблицы у настройки нет, значения читаются из конфигурации при старте приложения.

Со значением по умолчанию `LdapValidateServerCertificate = true` соединение на этих путях не устанавливается, если сертификат LDAPS-сервера не проходит проверку операционной системы сервера приложения — например, он самоподписанный или выдан корпоративным центром сертификации, которому она не доверяет. Отпечаток в `LdapTrustedCertificateThumbprints` такое соединение не пропускает: до сверки отпечатков дело не доходит, поэтому и ошибки сверки с обозначением пути в журнале нет. Для такой площадки добавьте корневой сертификат центра сертификации в доверенные на сервере приложения либо выставьте `LdapValidateServerCertificate = false` вместе с перечнем отпечатков — тогда вместо цепочки проверяется совпадение с перечнем. Выключать проверку цепочки с пустым перечнем не рекомендуется: подлинность сервера в этом случае не проверяется.

В `appsettings.json` отпечатки задаются массивом `Auth:Ldap:TrustedCertificateThumbprints`, в устаревшем `web.config` — плоским ключом `LdapTrustedCertificateThumbprints` в `appSettings` со значениями через запятую. Запись — SHA-256 отпечаток: после удаления двоеточий и пробелов должно остаться ровно 64 шестнадцатеричных символа, регистр не важен. Сверяются отпечаток самого сертификата сервера и каждого доступного элемента его цепочки, поэтому отпечаток центра сертификации распространяет доверие на все выданные им сертификаты — задавайте его, только если это соответствует вашей модели доверия.

Пока список пуст, в журнал один раз за запуск приложения пишется предупреждение по каждому из трёх путей. Непустой список работает строго: несовпадение отпечатка, некорректная запись в списке или отсутствующий сертификат отклоняют соединение, а в журнал попадает ошибка с обозначением пути и SHA-256 предъявленного сертификата; пароли подключения в сообщение не входят. При замене сертификата добавьте новый отпечаток заранее, не удаляя прежний, — тогда приниматься будут оба.

### 6.1. appsettings.json — PAT, Windows-аутентификация, Multifactor, cookies и SQL-диагностика

В этом разделе описаны параметры `appsettings.json`, управляющие PAT-токенами, Windows-аутентификацией, двухфакторной аутентификацией через Multifactor, поведением cookie и SQL-диагностикой.

| `PatMaxTokensPerUser` | `10` | Максимум активных PAT-токенов на пользователя |
| `PatMaxExpirationDays` | `365` | Макс TTL PAT в днях. `0` — разрешает бессрочные |
| `WinAuthHost` | `IIS` | Тип хостинга Windows-аутентификации. `IIS` — Integrated Windows Auth через IIS-модуль (Windows host). `Kestrel` — ASP.NET Core Negotiate scheme (Linux/Docker host, требует Kerberos keytab/SPN со стороны OS). `None` — Windows-auth выключена |
| `Negotiate:RequireKerberos` | `false` | При `true` принимается только Kerberos; NTLM-аутентификация отклоняется (`WinClaimsTransformation` на Windows, `NegotiateClaimsTransformation` на Linux). Нужно когда NTLM запрещён политикой безопасности. Полный путь: `Auth:Negotiate:RequireKerberos`. Доступно с v2.268.302 |
| `Multifactor.IsEnabled` | `true` | Двухфакторная через сервис MULTIFACTOR. При `false` точки входа второго фактора — запрос на вход и приём ответа от сервиса — сразу отвечают `503` с телом `MFA_DISABLED`, не доходя до обработки: выключение закрывает второй фактор, а не пропускает вход мимо него. Так же отвечает и включённый метод, если провайдер аутентификации неактивен или не существует |
| `Multifactor.Host` | — | Не используется: адрес, на который сервис MULTIFACTOR возвращает пользователя, строится из системного параметра «Путь к приложению» (`ApplicationPath`) |
| `ActiveDirectoryAuthenticationMode` | `ldap` | Библиотека для AD-аутентификации: `DirectoryServices` (System.DirectoryServices, только AD), `PrincipalContext` (более высокоуровневая, нужна при одноимённых учётных записях в лесе AD), `ldap` (Novell.Directory.Ldap — работает и с AD, и с OpenLDAP) |
| `AuthUseInsecureCookies` | bool / `false` | Разрешает не-secure cookie для аутентификации. ⚠️ Включать **только** при работе по HTTP. Для HTTPS — снижает безопасность |
| `SetCookieForUpperLevelDomain` | bool / `false` | Cookie `1FormaAuth` ставится на домен верхнего уровня (нужно при нескольких поддоменах, например `forms.example.ru` + `win.example.ru`). По умолчанию — на полный FQDN |

**Диагностика:**

```sql
-- Все активные PAT пользователя
select
        pat.Id,
        pat.Name,
        pat.TokenPrefix,
        pat.CreatedAt,
        pat.ExpiresAt,
        pat.LastUsedAt,
        pat.IsRevoked
from PersonalAccessTokens pat with (nolock)
where pat.UserId = @UserId
      and pat.IsRevoked = 0
order by pat.CreatedAt desc;
```

### 7. Поведение при отсутствии лицензии или превышении лимита сессий в OIDC

Если пользователь, успешно прошедший аутентификацию в Keycloak, не имеет лицензии на модуль **«Первая форма»**, или используется **конкурентная лицензия** `FirstFormaConcurrent` и лимит сессий исчерпан, вход в 1Форму через OIDC блокируется на этапе создания токена.

Что происходит:

| Ситуация | Результат |
|---|---|
| Нет именной лицензии `FirstForma` | Редирект на `/spa/entry/signin?authError=Нет лицензии для модуля Первая форма`. Токен не выдаётся. |
| Исчерпан лимит `FirstFormaConcurrent` | Редирект на `/spa/entry/signin?authError=…` с сообщением о превышении лимита сессий. |
| Лицензия в порядке | Обычный вход, сессия и токен создаются. |

Текст сообщения берётся из локали (`Language.doesNotHaveLicenceShort`) и отображаемого имени модуля `Module.FirstForma.GetDisplayName()` — аналогично парольному входу.

> **Примечание для администратора.** Сообщение появляется на странице входа 1Формы, а не в Keycloak. Если пользователь видит страницу входа 1Формы с ошибкой лицензии после успешного ввода учётных данных в Keycloak, проверьте лицензионные настройки пользователя в 1Форме (именная или конкурентная лицензия «Первая форма»).

### 8. Учёт конкурентных лицензий

Пользователи с лицензией «Первая форма (конкурентная)» делят общий пул слотов. Слот выдаётся на пользователя, а не на устройство или вкладку, и возвращается в пул по неактивности.

- **Занятие слота.** Слот запрашивается при доступе к 1Форме, если у пользователя нет именной лицензии на модуль. Один пользователь занимает не более одного слота независимо от числа открытых вкладок, клиентских приложений и устройств: в таблице `UserSessions` ему соответствует одна строка. Лимит — `ConcurrentLicensesCount` из файла лицензии, общий на всех пользователей.
- **Освобождение слота.** Пока пользователь активен, его сессия продлевается; по неактивности слот освобождается. Минимальную длительность активной сессии задаёт ключ `ConcurrentSessionMinLengthInMinutes` (значение по умолчанию — в таблице §6): меньшее значение быстрее возвращает лицензию в пул, большее дольше удерживает слот при кратковременном простое.
- **Исчерпание лимита.** Когда свободных слотов нет, запросы отклоняются с ответом `402`, в интерфейсе появляется баннер «Нет свободных лицензий» с кнопкой проверки доступности — до успешной проверки новые запросы не отправляются, после неё страница перезагружается и работа продолжается. Вход через OIDC в этом случае возвращает пользователя на страницу входа с сообщением о превышении лимита сессий (§7).

**Что видно администратору.** Счётчики лицензий выводятся на главной странице администрирования и на странице «Лицензии пользователей» ([§«Лицензии пользователей»](https://help.1forma.ru/domains/users-and-groups/admin.md#лицензии-пользователей)). Для конкурентной лицензии подпись счётчика — вида «Первая форма (конкурентные) N из M»: N — число пользователей, которым лицензия назначена, M — приобретённый лимит. Занятые в текущий момент слоты в интерфейсе не выводятся.

**Занятость и история.** Распределение слотов хранится как активные строки таблицы `UserSessions`, поэтому текущую занятость получают запросом к ней: отдельного отчёта или страницы для этого нет. История занятости не сохраняется — слот живёт до освобождения и не накапливается. Оценить потребление за прошедший период можно по журналу входов `dbo.LoginsLog`: он показывает входы по дням (см. §«Журналирование входов и изменений провайдеров»), но не одновременную занятость — один пользователь за день входит несколько раз, занимая при этом один слот.

## Пошаговые сценарии настройки сервисов

> Концепции и параметры протоколов — в разделе [«Аутентификация и авторизация»](https://help.1forma.ru/domains/auth/business.md). Ниже — UI-сценарии создания сервисов и провайдеров.

### Общий порядок, OpenLDAP, RADIUS и Multifactor

Настройка любого внешнего способа входа идёт в три шага:

1. Создать **сервис** (`system → подключения → Сервисы`) — выбрать тип, заполнить параметры подключения
2. Создать **провайдер аутентификации** (`authentication-providers`) — выбрать сервис, настроить MFA, группы, видимость
3. Настроить **AuthConfig** (пользовательский ключ) — определить доступные типы входа


**Сервис** (`openldap-service-settings`):

| Поле | Описание |
|------|----------|
| Адрес сервера | URL LDAP-сервера |
| DN путь | Базовый DN для поиска |
| DN привязки | DN учётной записи для bind |
| Пароль привязки | Пароль для bind |

SSL-подключение: ключ `UseSecureLDAP` в `web.config`/`appsettings.json`. Флаг действует на обе точки подключения OpenLDAP — проверку пароля пользователя и служебный bind под учётной записью администратора каталога. При выключенном флаге оба пароля передаются по сети открытым текстом.

> Параметры: [business.md#openldap](https://help.1forma.ru/domains/auth/business.md#аутентификация-через-openldap-и-radius)


**Сервис** (`radius-service-settings`):

| Поле | Описание |
|------|----------|
| IP-адрес | Адрес RADIUS-сервера |
| Порт | Порт (обычно 1812) |
| Shared secret | Общий секрет |

> Параметры: [business.md#radius](https://help.1forma.ru/domains/auth/business.md#аутентификация-через-openldap-и-radius)


**Сервис** (`multifactor-service-settings`):

1. В админ-панели Multifactor (`admin.multifactor.ru`): Настройки → Расширенное API → скопировать API Key и API Secret
2. В 1Ф: создать сервис типа Multifactor, указать API URL (`https://api.multifactor.ru`), API-ключ и Секретный ключ
3. Установить сертификат `<corp-domain>.cer` на сервер
4. В провайдере аутентификации: Второй фактор → Сервис → выбрать сервис Multifactor

**appsettings.json:** секция `Multifactor` с ключами `IsEnabled` и `Host`.
**web.config:** `MultifactorIsEnabled` и `MultifactorHost`.

**Часы сервера.** Срок действия и время выпуска токена, с которым сервис MULTIFACTOR возвращает пользователя, сверяются с часами сервера 1Формы с допуском 30 секунд. Если часы сервера расходятся с часами сервиса сильнее, вход через сервис может не завершаться: после подтверждения второго фактора пользователь возвращается на страницу входа.

> Процесс и параметры: [business.md#multifactor-сервис](https://help.1forma.ru/domains/auth/business.md#многофакторная-аутентификация-mfa)

### Active Directory

**Сервис** (`active-directory-service-settings`):

| Поле | Описание |
|------|----------|
| Домен | Имя домена компании |
| Домен является корнем леса | Указанный домен — корневой в лесу AD. Дерево каталога показывает все домены леса, дочерние домены раскрываются и выгружаются. Без флага сервис работает с одним доменом |
| Логин/Пароль для доступа к AD | Опционально — если пользователь приложения уже имеет нужные права |

⚠️ **Режимы аутентификации:** по умолчанию используется `DirectoryServices`. При наличии **одноимённых учётных записей в лесу AD** переключить на `PrincipalContext` через настройку `ActiveDirectoryAuthenticationMode` в `web.config`/`appsettings.json`. Без переключения система может авторизовать не того пользователя.

**Провайдер:** тип `ActiveDirectory`, выбрать созданный сервис, настроить второй фактор при необходимости.

> Параметры и бизнес-логика: [business.md#active-directory](https://help.1forma.ru/domains/auth/business.md#аутентификация-через-active-directory)

### SAML (ADFS)

**Сервис** (`saml-service-settings`):

| Поле | Описание |
|------|----------|
| IDP Metadata URL | URL метаданных IdP, например `https://your-domain.com/FederationMetadata/2007-06/FederationMetadata.xml` |
| Сопоставление пользователей | На данный момент — только по SID из Active Directory |

**Settings (JSON):**

| Ключ | Описание |
|------|----------|
| `Issuer` | Издатель запроса. ⚠️ Для ADFS **должен совпадать с доменом SP-приложения** (relying party trust в терминах ADFS) |
| `SignatureAlgorithm` | Алгоритм подписи (по умолчанию: `rsa-sha256`) |
| `UserClaimName` | Claim для идентификации пользователя. Для ADFS: `primarySid` |
| `SignAuthRequests` | Подписывать ли запросы к IDP. ⚠️ Для ADFS обязательно `true` |
| `SignCertificatePath` | Путь к `.pfx`-сертификату для подписания SP→IDP |
| `SignCertificatePassword` | Пароль к `.pfx`-файлу |
| `RenewalMode` | Способ продления сессии, одно из трёх значений. `IdentityProvider` — продление только новым ответом IdP (значение по умолчанию). `Activity` — продление по активности: пока пользователь работает, 1Форма перевыпускает токен сама, к IdP не обращается, но сессия ограничена абсолютным потолком. `None` — 1Форма продлевает токен самостоятельно без потолка, из-за чего сессия живёт дольше сессии на стороне провайдера. Прежний ключ `FederatedRenewalEnabled` продолжает читаться: `true` понимается как `IdentityProvider`, `false` — как `None`; при наличии обоих ключей побеждает `RenewalMode` |
| `AllowUnsolicitedResponse` | Принимать ли ответы IdP, не связанные с запросом от 1Формы (IdP-initiated SSO). По умолчанию `false` — такой ответ отклоняется. Включать только если вход действительно инициируется со стороны IdP |
| `CertificateValidationMode` | Режим проверки цепочки сертификатов IdP: `None`, `PeerTrust`, `ChainTrust`, `PeerOrChainTrust`, `Custom`. По умолчанию `ChainTrust` |
| `RevocationMode` | Режим проверки отзыва сертификатов IdP: `NoCheck`, `Online`, `Offline`. По умолчанию `Online` |

**Продление сессии.** Способ задаётся ключом `RenewalMode`.

В режиме `IdentityProvider` токен SAML-сессии 1Форма самостоятельно не перевыпускает — срок продлевается только по новому ответу IdP. Пассивный запрос `/api/auth/saml/{providerId}/passive` (AuthnRequest с признаком `IsPassive`) сервер поддерживает, но веб-интерфейс продление по нему не запускает, поэтому в браузере сессия в этом режиме не продлевается: по истечении токена пользователь проходит интерактивный вход через страницу IdP. Чтобы сессия продлевалась без повторного входа, задайте режим `Activity`.

В режиме `Activity` сессия продлевается по работе пользователя: пока обращения к API идут непрерывно, токен перевыпускается на стороне 1Формы без похода к IdP. Через перевыпуск переносятся данные разлогина, поэтому выход и single logout работают после любого числа продлений. Заголовок `1F-Auth-Renew: saml` в этом режиме не выставляется. Общая длительность ограничена абсолютным потолком `SamlSessionAbsoluteLifetimeMinutes`, который отсчитывается от выдачи первого токена 1Формы; по его достижении пользователь проходит полный интерактивный вход. Режим доступен только SAML-провайдерам и только при включённых скользящих токенах: при иной конфигурации на старте приложения пишется ошибка в журнал, а провайдер остаётся без продления.

В режиме `None` продление не отключается — 1Форма перевыпускает токен сама, без обращения к IdP и без абсолютного потолка, поэтому сессия в 1Форме живёт дольше сессии на стороне провайдера. Это не ужесточение сессии, а ослабление её связи с IdP. Refresh-cookie для SAML выдаётся только в этом режиме.

Списки провайдеров по режимам вычисляются при инициализации приложения — изменение `RenewalMode` применяется после перезапуска. Отдельного поля в форме провайдера для режима нет: он правится в `Settings (JSON)` секции SAML.

> У OIDC-провайдеров действует тот же ключ `RenewalMode` (кроме режима `Activity` — он только для SAML), но продление выполняется без участия браузера, refresh-grant'ом к token-endpoint.

**Сверка ответа с запросом.** Каждый запрос на аутентификацию, отправленный 1Формой в IdP, запоминается на 10 минут. Ответ IdP принимается, только если он ссылается на такой запрос, и только один раз: повторный ответ с той же ссылкой, а также ответ по просроченному или неизвестному запросу отклоняются. Сверка выполняется после проверки подписи ответа, то есть дополняет её, а не заменяет.

Ответ, вовсе не связанный с запросом (вход инициирован на стороне IdP), по умолчанию отклоняется. Если такой сценарий используется, у провайдера нужно включить `AllowUnsolicitedResponse` — иначе вход перестанет работать. Провайдеры, где вход всегда начинается из 1Формы, настройки не требуют.

**ClaimsMapperConfig:**

| Ключ | Описание |
|------|----------|
| `IdentityClaim` | Атрибут для однозначной идентификации: `Email`, `Nick`, `ExternalAccount`, `SID` |
| `CreateProfiles` | `true` → автосоздание профиля из SAML-атрибутов, если УЗ не найдена по IdentityClaim |
| `ProfileAttributesMap` | Маппинг полей профиля (см. ниже) |

**ProfileAttributesMap — 29 полей:**
`Nick`, `LastName`, `FirstName`, `MiddleName`, `FullName`, `Phone`, `Phone2`, `Phone3`, `Email`, `ExternalEmail`, `DisplayName`, `Position`, `EnglishDisplayName`, `ExternalDisplayName`, `CellPhone`, `HomePhone`, `Fax`, `Skype`, `ICQ`, `LiveJournal`, `Twitter`, `IsEmployee`, `BirthDate` (datetime), `WorkStartDate` (datetime), `Country`, `City`, `Gender` (bool), `Notes`, `SID`, `GuidFrom1C` (guid), `PhoneAdditional`, `Phone2Additional`, `Phone3Additional`, `HomePhoneAdditional`, `MaidenName`, `CanEditAvatar` (bool), `TelegramUserName`

> Если для SAML включена подпись запросов, но сертификат подписи не задан или не читается (пустой путь, файла нет, неверный пароль), либо некорректны метаданные IdP, вход через SAML не работает — провайдер сообщает, что интеграция не настроена. Проверьте путь к файлу сертификата и пароль, права на чтение файла и адрес метаданных IdP.

⚠️ `CreateProfiles: true` = автопровизионинг пользователей без предварительного создания в 1Ф. Без понимания ограничения `Issuer` — SSO с ADFS не заработает.

**appsettings.json:** `SamlEntityId` (доменное имя SP), `SamlCertificatesRoot` (путь к сертификату).

**Маршруты:** `/api/auth/saml/{providerId}/login`, `/api/auth/saml/{providerId}/assertionconsumer`, `/api/auth/saml/{providerId}/logout`, `/api/auth/saml/{providerId}/singlelogout`.

Метаданные SP: `/api/auth/saml/{providerId}/metadata`.

> Концепции и SSO-поток: [business.md#saml](https://help.1forma.ru/domains/auth/business.md#аутентификация-через-saml)

### OAuth / OIDC (KeyCloak, v2.263+)

**Сервис** (`oauth-service-settings`):

| Поле | Описание |
|------|----------|
| Alias | Алиас провайдера `[a-z]` |
| Реализация OAuth | OpenIDConnect |
| ClientId | ID приложения KeyCloak (Clients → компания → General → Client ID) |
| ClientSecret | Секретный ключ (Clients → компания → Credentials → Client secret) |
| Сопоставление пользователей | Один из 4 типов (см. ниже) |

**Settings (JSON):**

| Ключ | Описание |
|------|----------|
| `AuthorityUrl` | URL центра OAuth |
| `ResponseType` | Тип ответа AtomID |
| `ResponseMode` | Режим ответа |
| `CallbackPath` | URL возврата после аутентификации |
| `Claims` | Список claims |
| `ClaimsMapperConfig` | Настройки маппинга (см. ниже) |

**5 типов ClaimsMapper:**

| Класс | Описание |
|-------|----------|
| `ClaimsMapperByDictionary` | По справочнику (категории) |
| `UserClaimsMapperByAttribute` | По произвольному атрибуту |
| `UserClaimsMapperByExternalAccounts` | По УЗ внешнего сервиса (UserExternalAccounts) |
| `UserClaimsMapperByNickname` / по SID | По Nickname или SID |
| `UserClaimsMapperByUpn` | По UPN из Windows-claim. Используется по умолчанию в `NegotiateSettings.UserMapper` для Kerberos/Negotiate-входа; `ClaimsMapperConfig.UserProfileAttribute` задаёт поле пользователя для маппинга (по умолчанию `Nick`) |

**Сопоставление по учётной записи внешнего сервиса.** `UserClaimsMapperByExternalAccounts` ищет в `dbo.UserExternalAccounts` строку того же сервиса со значением `ExternalUid`, пришедшим в claim. Сравнение выполняет СУБД по коллации колонки, приложение регистр не приводит: под регистронезависимой коллацией MS SQL `alice` и `Alice` — одно значение, и хвостовые пробелы при сравнении не учитываются; на PostgreSQL значимы и регистр, и хвостовые пробелы. При создании новой привязки `ExternalUid` сохраняется без хвостовых пробелов, а `ExternalAccount` — как пришло. Если под коллацией подходит несколько строк, берётся та, что совпадает с пришедшим значением посимвольно; если такой ровно одной строки нет, вход отклоняется с ошибкой неоднозначности.

**ClaimsMapperConfig:**

| Ключ | Описание |
|------|----------|
| `IdentityClaim` | `Email`, `Nick`, `ExternalAccount`, `SID` |
| `DictionarySubcatId` | ID категории справочника (для ClaimsMapperByDictionary) |
| `DictionaryIdentityExtParamId` | ID ДП для идентификации |
| `DictionaryUserIdExtParamId` | ID ДП для UserID |

**После настройки:**

1. Добавить провайдер аутентификации с созданным сервисом
2. Прописать алиас в ключе `OidcAliases` в `appsettings.json` (несколько провайдеров — через запятую)

**Логирование:** вход через OIDC логируется в `LoginsLog` аналогично Forms-аутентификации (сброс счётчика попыток). Выход не логируется.

> Концепции и способы сопоставления: [business.md#oauth-20--openid-connect](https://help.1forma.ru/domains/auth/business.md#аутентификация-через-oauth-20--openid-connect)

## Настройка типов входа (AuthConfig)

Пользовательский ключ `AuthConfig` управляет доступными способами входа на форме авторизации.

**По умолчанию:** `{"AuthTypes": []}`

**Параметры каждого элемента AuthTypes:**

| Параметр | Описание |
|----------|----------|
| `Type` | `login-pass`, `phone-code`, `email-code`, `external-provider` |
| `IsDefault` | Тип входа по умолчанию. При нескольких `true` — появляется кнопка переключения |
| `AllowRegister` | Показать кнопку регистрации |
| `AutoRegister` | Автоматическая регистрация |
| `Visibility` | `all`, `web`, `mobile` — ограничение по устройству |
| `PrivacyLink` | Ссылка на соглашение при входе |
| `RegisterPrivacyLink` | Массив ссылок на соглашение при регистрации (`linkUrl` + `linkTitle`) |
| `RegistrationType` | `all`, `phone`, `email` |
| `HideProviders` | Скрыть выбор провайдера |

⚠️ Параметры в AuthTypes **обязательно с заглавной буквы**.

**Как работает `Visibility`.** Значение ограничивает способ входа по типу устройства: `mobile` — только мобильные (Android/iOS), `web` — только компьютеры, `all` (или не указано) — все устройства. Устройство определяется по фактической платформе браузера (Android/iOS), а не по режиму отображения страницы: способ с `Visibility: "mobile"` виден на реальном мобильном устройстве независимо от того, включён ли режим mSPA.

**Вход по email (`email-code`).** Пользователь вводит адрес почты, указанный в его профиле, и нажимает «Далее»; на этот адрес приходит код подтверждения, ввод которого завершает вход — код вводится целиком, длина определяется серверным сценарием. Для работы способа в профиле пользователя должен быть добавлен почтовый ящик, а в общих настройках приложения — выбран почтовый ящик для системных писем и настроен SMTP-сервер.

![Окно входа по почте: поле ввода адреса и кнопка «Далее»](https://help.1forma.ru/help-images/auth/auth_type_4.png)

**Вход по номеру телефона (`phone-code`).** Пользователь вводит номер телефона и нажимает «Далее»; на него приходит код подтверждения, ввод которого завершает вход — код вводится целиком, длина определяется серверным сценарием. Требуется настроенный SMS-провайдер.

![Окно входа по номеру телефона: поле ввода номера и кнопка «Далее»](https://help.1forma.ru/help-images/auth/auth_type_1.png)

**Поведение `external-provider`:** отображается экран только с кнопками внешних провайдеров (без формы логина/пароля и капчи). Внизу — ссылка «Войти по логину и паролю» с кнопкой «Назад». ⚠️ **Если в AuthTypes нет `login-pass` — ссылка не отображается.**

**Приоритет:** AuthConfig > `sys_general_settings` — если регистрация отключена в ключе, глобальная настройка «Разрешена регистрация» не поможет.

**Время жизни кода:** `AuthLoginCodeExpiresInMinutes` в `appsettings.json` → `Auth` (по умолчанию 5 минут). Код регистрации/аутентификации — 6-значный и одноразовый: повторно использовать один код нельзя даже при параллельных запросах.

**Балансировщик нагрузки:** без настройки `ForwardHeaders` все запросы воспринимаются как один IP → массовая блокировка.

```json
"ForwardHeaders": {
  "Headers": "For,Proto,Host,Prefix",
  "Networks": "*",
  "Proxies": "*"
}
```

> Полная JSON-схема и типы входа: [business.md#способы-входа-на-форме-авторизации](https://help.1forma.ru/domains/auth/business.md#способы-входа-на-форме-авторизации)

## Самостоятельная регистрация (RegistrationFields)

Ключ `RegistrationFields` определяет набор полей на форме самостоятельной регистрации.

**9 кодов полей:**

| Код | Тип | Описание |
|-----|-----|----------|
| `Email` | email | Адрес почты |
| `CellPhone` | phone | Мобильный телефон |
| `Nick` | string | Псевдоним |
| `FirstName` | string | Имя |
| `LastName` | string | Фамилия |
| `Gender` | select | Пол (1 = Мужской, 0 = Женский) |
| `City` | string | Город |
| `Password` | password | Пароль |
| `Note` | string | Примечание (поддерживает HTML; доп. параметры `title` и `Color`) |

Поле `Note` выводит на форму регистрации произвольный текстовый блок с поддержкой HTML-разметки; его заголовок и цвет текста задаются параметрами `title` и `Color`.

![Пример текстового примечания на форме регистрации](https://help.1forma.ru/help-images/auth/registration_note_1.png)

**Структура элемента:**
```json
{"Key": "Email", "isRequired": true/false, "IsHidden": true/false}
```

**PasswordSettings** (вложенная секция для поля Password) — 11 параметров:

| Параметр | Описание |
|----------|----------|
| `MinNumberOfChar` | Минимальное количество символов |
| `MaxNumberOfChar` | Максимальное количество символов |
| `UpperLowercaseRequired` | Заглавные и строчные обязательны |
| `AcceptableLanguage` | Допустимый язык (например `ru-RU`) |
| `MinNumberOfDigits` | Минимум цифр |
| `NoSpaces` | Без пробелов |
| `MinNumberOfSpecialChar` | Минимум спец. символов |
| `DisallowLoginOrBirthdayPattern` | Запрет логина/даты рождения в пароле |
| `DisallowedSequenceLength` | Запрет последовательностей (йцукен, qwerty, 123) |
| `DisallowedRepeatingPatternLength` | Запрет повторяющихся паттернов (qweqwe, 123123) |
| `NumberOfPreviousPasswordsToCheck` | Проверка истории паролей (рекомендуется ≥ 10) |
| `MinCharPasswordDifference` | Мин. отличие от предыдущего пароля (рекомендуется ≥ 5) |

⚠️ PasswordSettings действует **только** на форму самостоятельной регистрации. Для создания пользователей из режима администрирования и сброса паролей — `sys_general_settings`.

**7 правил регистрации:**

1. Одно из `CellPhone`, `Email`, `Nick` — обязательно
2. Поле, использованное для верификации (телефон/email), скрывается на форме
3. При заполнении `Nick` — пароль обязателен
4. Пустой `Nick` автозаполняется из Phone или Email
5. Пустой `DisplayName` автозаполняется из `FirstName LastName` или `Nick`
6. При заданном `CellPhone` без `Email` (если Email обязателен) — `phone@domain`
7. `Gender` по умолчанию = 1 (Мужской)

**Предзаполнение через URL:** `~/spa/entry/signup?{RegistrationCode}={значение}`

**Параметр `source`:** `spa_base` (веб) или `mobile` (МП) — используется в смарт-событии «После создания пользователя».

> Автозаполнение и бизнес-правила: [business.md#самостоятельная-регистрация](https://help.1forma.ru/domains/auth/business.md#самостоятельная-регистрация)

## Синхронизация каталогов: настройки и связь с сервисами

> **Недоступно в 1F Certificate Edition (ФСТЭК-сборка).** Синхронизация с каталогом в сборку не входит: задача ежедневного запуска не зарегистрирована, а маршруты `api/sync-ad`, `api/admin/ldap`, `api/admin/users/{id}/sync-with-ad` и `api/admin/groups/domains` отвечают 404. Раздел администрирования синхронизации в интерфейсе остаётся (фронт по решению владельца не менялся) и при обращении к снятым маршрутам вернёт ошибку 404 — это ожидаемое поведение, а не дефект. Профили синхронизации и записи расписания в базе остаются, но больше не читаются. Настройки полей профиля можно создавать и хранить, они не запускают синхронизацию.

**Где настраивается:** `synchronizations`, `synchronization-settings`.

**Таблицы БД:**

- `dbo.SynchronizationProfiles`
- `dbo.SynchronizationProfilesADSettings`

**Что контролируется:** импорт пользователей/групп и консистентность каталога для auth-проверок.


Профиль синхронизации (`synchronizations`) связывается с сервисом через поле **Сервис** — выбирается один из настроенных AD/LDAP-сервисов.

Ручные входы в администрировании — импорт пользователей из каталога и дерево подразделений каталога — работают с профилем любого типа, включая OpenLDAP: подключение строится по типу профиля, а объекты читаются по схеме каталога, поэтому доменный контроллер профилю OpenLDAP не нужен.

⚠️ Для каждого сервиса может быть только **одна активная** настройка синхронизации.

**Ключевые параметры профиля:**

| Параметр | Описание |
|----------|----------|
| Синхронизация с AD | Синхронизация по расписанию `ADSyncJob` — в 1F Certificate Edition параметр не действует, задача в сборку не входит |
| Данные пользователей | Синхронизация полей профиля |
| Названия групп | Синхронизация имён групп |
| Создание новых групп | Авто-создание групп из AD |
| Членство пользователей | Синхронизация состава групп |
| Вложенность групп | Синхронизация иерархии групп |
| Создавать пользователей из AD | Авто-создание новых пользователей |
| Логины в формате Domain\Nick | Для работы в нескольких доменах с одинаковыми никами |
| Только орг. единиц из AD | Сохраняет ручные орг.единицы при синке |
| Маска для создания групп | Фильтр групп по маске |
| Организационные единицы | Дерево OU для синхронизации |
| AD фильтр пользователей | LDAP-фильтр пользователей (до 1000 символов) |
| AD фильтр групп | LDAP-фильтр групп |
| Макс. допустимое кол-во обновлений | Лимит — при превышении задание не выполняется |

**Маппинг полей (встроенные):** `company` → Компания, `department` → Отдел, `description` → Должность, `givenName` → Имя, `displayName` → Псевдоним (рус.), `telephoneNumber` → Рабочий, `mail` → E-mail, `thumbnailPhoto` → Аватар.

⚠️ Названия полей **регистрозависимые** — `displayname` и `DisplayName` распознаются как разные параметры.

> Детали синхронизации и AD-ограничения: [../users-and-groups/business.md](https://help.1forma.ru/domains/users-and-groups/business.md)

## CustomSettings — прочие ключи аутентификации

Дополнительные пользовательскые настройки приложения, влияющие на вход:

| Ключ | Тип | Назначение |
|------|-----|------------|
| `ForbidTCLogin` | `0` / `1` | Если `1` — вход в приложение TaskCenter (платформа .NET Framework, legacy-клиент) проверяется на права. Если `0` — войти может любой пользователь. Используется для блокировки legacy-доступа |
| `PasswordRecoveryText` | string (Markdown) | Пользовательскый текст на странице восстановления пароля (`/spa/entry/password-recovery`). Markdown-форматирование поддерживается |
| `LogRefreshTokenRequests` | bool | Если `true` — обновления токенов мобильного клиента логируются в `LoginsLog` и отображаются в журнале пользователя/логах МП наряду с обычными входами. Используется для аудита |

## Типичные ошибки настройки

Частые проблемы входа, их причины и способ проверки:

| Симптом | Причина | Где проверить | SQL-диагностика |
|---------|---------|---------------|-----------------|
| На форме логина нет нужного провайдера | Провайдер неактивен/скрыт/не назначен по умолчанию | `authentication-providers` | `select ProviderID, Name, ProviderType, IsActive, IsHidden, IsDefault from dbo.AuthenticationProviders order by ProviderID;` |
| Пользователь видит провайдер, но вход запрещён | Нет привязки группы в `AuthenticationProviderGroups` | Доступ к провайдеру по группам | `select * from dbo.AuthenticationProviderGroups where ProviderId = @ProviderId;` |
| LDAP/AD поиск пользователей в админке не работает | Неверные учётные данные или адрес сервиса | `active-directory-service-settings`, `openldap-service-settings` | `select * from dbo.LDAPServicesCredentials; select * from dbo.OpenLDAPServicesCredentials;` |
| Согласование SAML/OAuth завершается ошибкой | Ошибка metadata/issuer/cert/callback-path | `saml-service-settings`, `oauth-service-settings` | `select * from dbo.SAMLServicesSettings; select * from dbo.OAuthServicesSettings;` |
| Вход блокируется после серии ошибок | Слишком жёсткие лимиты попыток / captcha | `system-settings` | `select FailedAttemptsCount, LastUpdate from dbo.IpList where Ip = @Ip; select LogFailsCount from dbo.Users where Nick = @Login;` |
| PAT-токен не работает (401) | Токен отозван, просрочен или повреждён | `/api/*/pat/list` или БД | `select TokenPrefix, IsRevoked, ExpiresAt, LastUsedAt from PersonalAccessTokens where UserId = @UserId;` |
| Пользователь не может генерировать PAT | Нет спецправа `GENERATEPAT` | спецправа группы | Проверить наличие права `GENERATEPAT` на группу пользователя |
| Лимит токенов исчерпан | `PatMaxTokensPerUser` достигнут | `appsettings.json` | `select count(*) from PersonalAccessTokens where UserId = @UserId and IsRevoked = 0;` |

## Аутентификация API-запросов

Способы аутентификации при обращении к API:

| Метод | Заголовок | Когда использовать |
|-------|-----------|-------------------|
| PAT | `1F-Pat: {token}` | Production-окружение |
| JWT | `1FormaAuth: {jwt}` | Тестовые стенды — PAT не работает |
| Cookie | `1FormaAuth={token}` | Браузерные сессии |

**`Authorization: Bearer {token}` → всегда 401.** Не использовать.

### Наблюдаемость запросов с токеном в заголовке (с 2.268)

Запросы, которые браузер выполняет с access-токеном в заголовке `1FormaAuth` и без cookie `1FormaAuth`, сервер считает по узлу. Раз в час по каждому ключу «пользователь + маршрут + браузер + источник» в журнал действий пользователя добавляется одна запись с числом запросов за час; при большом числе ключей (больше 200 на узел) остальные сводятся в запись «прочие». Запросы с cookie, от мобильных приложений, без заголовка User-Agent и с невалидным токеном не учитываются. Ответы API не меняются — это только наблюдение.

Так администратор видит, кто вне SPA пользуется каналом с токеном в заголовке, пока канал не снят и не включён строгий режим CSRF.

### Стенды vs Production и Python SSL на стендах

Различия аутентификации между боевым окружением и тестовыми стендами:

| Аспект | Production-окружение | Тестовые стенды |
|--------|---------------------------|------------------------|
| PAT | Работает | Не работает → только JWT |
| SSL | Доверенный сертификат | Self-signed → `verify=False` / `-k` |
| Авторизация | `1F-Pat: {token}` | `1FormaAuth: {jwt}` через `POST /api/auth/token-v2` |


Python 3.9 не использует macOS Keychain → `ssl.create_default_context()` с `check_hostname=False`, `verify_mode=CERT_NONE`.

## Конфигурация Windows-аутентификации (IIS / Kestrel Linux)

### Параметр WinAuthTokenExpiresInMinutes (v2.267.376+)

Время жизни access-токена для Windows-аутентификации (IIS Integrated Windows Authentication) задаётся параметром `Auth.WinAuthTokenExpiresInMinutes` в `appsettings.json`.

**Поведение:**

- Если параметр не задан (`null`) — используется значение из `Auth.AuthTokenExpiresInMinutes` (по умолчанию 1500 мин = 25 ч).
- Если задан — используется указанное значение (в минутах).

**Пример:**

```json
{
"Auth": {
"WinAuthTokenExpiresInMinutes": 1500
}
}
```

**Когда настраивать.** Параметр добавлен после инцидента, в котором TTL Win-токена был жёстко зашит на 5 минут — это вызывало попап ввода пароля Windows в IIS каждые 5 минут. Параметр позволяет явно выровнять TTL Win-токена с обычным token lifetime.

### Kerberos на Linux (Kestrel, v2.268.302+)

С v2.268.302 поддерживается нативная Kerberos-аутентификация на Linux: ASP.NET Core схема Negotiate (`auth.AddNegotiate()`) включается, когда `Auth:WinAuthHost = "Kestrel"`. Платформа не поднимает IIS; за согласование Kerberos (SPNEGO/GSSAPI) отвечает Microsoft.AspNetCore.Authentication.Negotiate, инфраструктура (`krb5.conf`, keytab с SPN сервиса, `KRB5_KTNAME`) настраивается стандартными средствами Linux/контейнера — в коде платформы этого нет.

**appsettings.json:**

```json
{
  "Auth": {
    "WinAuthHost": "Kestrel",
    "Negotiate": {
      "RequireKerberos": false,
      "UserMapper": "UserClaimsMapperByUpn",
      "MapperConfig": {
        "UserProfileAttribute": "Nick"
      }
    }
  }
}
```

`Auth:Negotiate:RequireKerberos: true` запрещает откат на NTLM — попытки NTLM-аутентификации отклоняются, и в лог пишется предупреждение «NTLM authentication rejected (RequireKerberos=true)». Использовать только когда KDC и SPN-инфраструктура гарантируют согласование Kerberos для всех клиентов; иначе пользователи получат ошибку входа.

**Endpoint статуса** — `GET /api/admin/auth/kerberos/status` (требует прав администратора), возвращает `KerberosStatusDto`:

| Поле | Значение |
|---|---|
| `winAuthHost` | значение из `Auth:WinAuthHost` (`IIS` / `Kestrel` / `None`) |
| `negotiateSchemeRegistered` | `true`, если ASP.NET Core зарегистрировал схему `Negotiate` (для Linux — индикатор что `AddNegotiate()` отработал на старте) |
| `requireKerberos` | значение из `Auth:Negotiate:RequireKerberos` |
| `claimsTransformationType` | активный класс трансформации: `WinClaimsTransformation` (Windows) или `NegotiateClaimsTransformation` (Linux) |
| `platform` | ОС сервера, например `Ubuntu 24.04.4 LTS` или `Microsoft Windows` |

Если `winAuthHost = "Kestrel"`, но `negotiateSchemeRegistered = false` — приложение стартовало без Windows-аутентификации, нужно проверить, как настраивается host.

**Тест маппинга UPN → пользователь** — `GET /api/admin/auth/kerberos/test-mapping?upn={upn}&attribute={attribute}` (по умолчанию `attribute=Nick`). Без реального согласования Kerberos проверяет, найдёт ли настроенный маппер пользователя по переданному UPN: вызывает `UserClaimsMapperByUpn` с заданным `attribute` как полем профиля и возвращает найденного пользователя (либо признак «не найден»). Используется для верификации `ClaimsMapperConfig.UserProfileAttribute` до подключения Kerberos.

**Логирование протокола.** При каждом входе через Negotiate в колонке `LoginsLog.AuthProtocol` сохраняется фактически использованный протокол (`Kerberos` / `NTLM` / `Negotiate` для остальных случаев) — основная диагностика «почему SSO не Kerberos» делается через эту колонку.
