Фаза 7, AGENT.md 7.1: провайдеры включаются переменными окружения
OAUTH_<PROVIDER>_CLIENT_ID/SECRET, привязка внешнего аккаунта идёт по
подтверждённому провайдером email через blind index, вход забаненному на
инстансе запрещён, все входы и привязки попадают в события безопасности,
привязка — ещё и в аудит инстанса.
Внешний идентификатор (subject) хранится только индексом HMAC-SHA-256,
токены провайдеров не сохраняются вовсе; state подписывается ключом сессий
и ограничен по времени, редирект после входа — только внутренний путь.
Если провайдер не настроен, ручка отвечает oauth.provider_not_configured,
а клиент показывает человеческий текст вместо кнопки.
Клиент: кнопки входа по списку включённых провайдеров на экране входа и
раздел «Вход через внешние сервисы» в настройках безопасности; адрес
возврата для настроек приложения виден в GET /auth/oauth/providers.
Миграция 00019 добавляет таблицу oauth_accounts. Установщик и .env.example
знают про OAUTH_* и сохраняют значения при --reconfigure.
Тесты: Go — выключенный провайдер, подписанный state (подмена и чужой
провайдер), создание и повторный вход, привязка к существующему аккаунту,
неподтверждённый email, выключенная регистрация, бан инстанса, mocked
провайдер (httptest) для обмена кода; web — кнопки провайдеров и ошибка
возврата; серверная ручка отдаёт понятный отказ без настроек.
Фаза 7, AGENT.md 7.1: регистрация ключа в настройках безопасности с
обязательным step-up, вход по ключу без пароля (в т.ч. без ввода почты —
обнаруживаемый ключ), несколько ключей на аккаунт, отзыв и переименование,
события безопасности и аудит.
Сервер: github.com/go-webauthn/webauthn (BSD-3-Clause), RP ID и Origin
берутся из конфига домена; если домен — IP-адрес (стенд без домена),
passkeys честно выключены (auth.passkey_unsupported). Церемонии живут в
памяти процесса 5 минут и одноразовые: повторная отправка challenge
отклоняется. Миграция 00018 пересобирает неиспользуемую таблицу
webauthn_credentials под полную запись credential в JSON. Новые ручки
входа ограничены по IP (10/мин).
Клиент: тонкая обёртка над navigator.credentials без тяжёлых SDK,
раздел «Ключи доступа» в настройках безопасности и кнопка «Войти по ключу»
на экране входа; понятные сообщения для браузеров без поддержки WebAuthn
и при отмене диалога.
Тесты: Go — регистрация/вход с эмулятором аутентификатора (реальная
проверка подписи P-256), отказ при чужом challenge, одноразовость
церемонии, обязательный step-up при управлении ключами, запрет входа
забаненному, ограничение allowCredentials при входе с почтой, лимит и
валидация имени; web — 12 vitest с моком navigator.credentials;
Playwright — живой сценарий с виртуальным аутентификатором Chromium.
- клиент откладывает перечитывание комнат (окно 250 мс) и не запускает второй
запрос, пока идёт первый: серия событий ролей и участников больше не даёт
лавину GET members/roles и 429 на лимите API;
- неожиданные ошибки API пишутся в лог (и в chi-, и в huma-ветке): под
параллельной нагрузкой на стенде видели разовые 500 на GET members/roles,
но без текста ошибки разобраться было нельзя.
Сервер:
- миграция 00017: таблица instance_bans (причина, автор, дата);
- auth: ErrUserBanned, проверка бана в Login (код user.banned, 403) и в
ResolveSession — забаненный не получает сессию ни по cookie, ни по Bearer,
ни в Gateway, а прежняя сессия удаляется;
- store: BannedAt/BanReason в модели пользователя, BanInstanceUser,
UnbanInstanceUser, IsInstanceBanned, поиск и фильтр в ListUsers,
CountUsersFiltered; мягкое удаление аккаунта убирает и запись о бане;
- API: POST /instance/users/{id}/ban и /unban со step-up (AGENT.md 7.1),
отзыв сессий и SESSION_INVALIDATED, аудит instance.user_ban с причиной и
instance.user_unban; себя и инстанс-админа забанить нельзя;
- GET /instance/users: q (логин и отображаемое имя), banned=true, total.
Клиент:
- панель: поиск, фильтр «только забаненные», бейдж бана с причиной, кнопки
«Забанить» (с причиной) и «Разбанить» через общий шаг подтверждения
личности; i18n ru/en, включая текст ошибки user.banned;
- keepPreviousData в списке пользователей: без этого поле поиска
размонтировалось на первом же символе и набор обрывался.
Тесты: 4 Go-теста (бан блокирует вход и сессии, защита админов, поиск,
уборка бана при удалении), web-тест панели, живая проверка на стенде 19/19.
- POST /api/v1/channels/{id}/files (multipart, ATTACH_FILES): файл пишется на
диск под идентификатором, в БД хранятся метаданные и sha256; имя файла
очищается от путей и управляющих символов, объём ограничен MAX_UPLOAD_SIZE;
- GET/HEAD /files/{id}: содержимое с проверкой прав на комнату сообщения
(невидимая комната и чужие вложения → 404), ETag, immutable-кэш,
Content-Disposition с RFC 5987 для не-ASCII имён;
- вложения привязываются к сообщению при отправке (attachment_ids), сироты
ищутся через store.ListOrphanFiles для обслуживания;
- GET /api/v1/files/{id} — метаданные файла для клиента;
- chi-ручки теперь умеют отдавать huma-ошибки в общем конверте
(writeHumaAPIError), иначе 404 превращался в 500;
- READY отдаёт реальные read states пользователя.
Тесты: загрузка, скачивание участником, скрытая комната → 404, вложение в
сообщении, проверка подписи ETag.
2FA обязательна для инстанс-администраторов, но до её включения вход закрыт —
получался замкнутый круг. Добавлены команды обслуживания:
- `glchat totp-setup --email <admin>`: создаёт секрет, подтверждает его кодом,
печатает секрет, otpauth-ссылку и 8 резервных кодов (каждый одноразовый);
- `glchat totp-reset --email <admin>`: удаляет секрет при потере устройства;
- код `auth.2fa_enrollment_required` с подсказкой, какую команду выполнить;
- установщик: флаг `--admin-2fa` для автоматического включения (по умолчанию
печатает подсказку, чтобы секреты не оседали в логах установки).
Проверено сквозным прогоном локально: bootstrap → 403 на входе без 2FA →
totp-setup → вход с TOTP-кодом → профиль, серверы, комнаты, роли, участники,
аудит, создание сервера админом, 429 на шестой попытке входа, секретов в логах
нет.
- переход на chi + huma (решение D-006): huma отдаёт типизированные ручки и
генерирует документ, auth-ручки живут на chi (cookie и заголовки напрямую)
- единый формат ошибок: код (auth.invalid_credentials, auth.2fa_required,
perm.denied и т.д.) + человекочитаемое сообщение (AGENT.md 8.5)
- cookie сессии __Host-session: HttpOnly, SameSite=Lax, Secure при TLS;
альтернатива — Bearer-токен для desktop/CLI (AGENT.md 8.1)
- ручки: register, login, logout, logout-all, sessions, step-up, 2fa/setup,
2fa/enable, users/@me; IP и User-Agent прокидываются из запроса в контекст
- /api/v1/openapi.json: объединённый документ (схемы huma + контракт auth)
- тесты: регистрация через API с cookie, ошибки входа, обязательная сессия,
валидация, наличие всех путей в OpenAPI