← Auth / Security

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

auth / security · workzone
1 Каналы входа Через какой канал выполняется вход.
Web UI
Admin Panel и Web App · email + пароль → /login → JWT. Единственный парольный вход.
Slack → Slack
Slack ID → identity_mapping (source='slack') → user: авто-матч по email воркспейса или код-привязка.
Telegram → Telegram
Telegram ID → identity_mapping (source='telegram') → user: только код-привязка — email Telegram не отдаёт.
Mattermost → Mattermost
Mattermost ID → identity_mapping (source='mattermost') → user: только код-привязка — email в событиях нет.
MCP → MCP
API-ключ (headless) или OAuth (интерактивно, v2) → user_id.
Browser Extensionv2 → Browser Extension
OAuth или токен, хранится в расширении.
Принцип: каждый канал аутентифицирует по-своему, но все сходятся один user_id роль ACL.
2 Идентификация и регистрация Откуда берётся user_id.
Модель admission — кто ручается → Онбординг пользователей
Как пользователь попадает в систему, определяет якорь доверия — кто за него ручается. Три якоря, по возрастанию масштаба:
· админ → приглашение (invite flow), точечно или пачкой CSV — безопасный дефолт;
· корпоративный IdPSSO, автопровижининг при входе — потолок масштаба, v2;
· почта компании → доменное самоприсоединение (любой с адресом домена), v2.
Решение: пол — контроль (invite, закрытый дефолт), потолок — делегирование IdP (SSO). Чтение базы знаний само по себе чувствительно, поэтому публичной саморегистрации нет; доменное самоприсоединение — не ступень между ними, а опция за явным тумблером (никогда не default-on, с предохранителями: подтверждённое владение доменом, очередь на одобрение, роль Member по умолчанию).
Setup Wizard — первый запуск → Мастер настройки
При 0 пользователей → /setup. Owner задаёт email + пароль. После создания /setup отдаёт 404.
защита от состояния гонки
Advisory lock на bootstrap — создание первого Owner. Один ключ закрывает оба входа (Setup Wizard и init-owner).
CLI init-owner
achilles init-owner --email … --password …. Только при 0 пользователей. Для CI/CD.
защита от состояния гонки
Тот же путь bootstrap, что и Setup Wizard, — защищён тем же advisory lock.
Owner / Admin создаёт приглашение (email + роль) → одноразовый invite-токен (48 ч) → ссылка уходит письмом через SMTP → регистрация (имя + пароль). Письмо — единственный канал доставки, вручную ссылка не выдаётся — доставка и служит верификацией email.
Роль Owner в приглашении назначает только Owner — Admin приглашает лишь Member (сервер отвечает 403 FORBIDDEN).
Пока SMTP не настроен, приглашение не создать: UI блокирует кнопку, сервер дублирует guard — 409 SMTP_NOT_CONFIGURED.
Массовая выдача (CSV) — пачка одноразовых токенов той же механики; доставку ведёт транспорт Email (фоновая очередь, безопасный темп), импорт идемпотентен — повторная заливка того же списка не плодит приглашений. UI — массовый импорт.
Сброс администратором
Owner сбрасывает Admin / Member, Admin — только Member, через Admin Panel.
Пароль самого Owner сбрасывает второй Owner, а если его нет — CLI на сервере (achilles reset-password --email …).
механизм
Основной путь — одноразовая reset-ссылка письмом (TTL 1 ч, в БД — только хэш), общая машинерия с flow «забыл пароль»; refresh-токены гасятся при установке нового пароля.
Фолбэк без настроенного SMTP — временный пароль (CSPRNG, показывается админу один раз), все refresh-токены гасятся сразу; сменить пароль пользователь может сам в профиле.
Принудительная смена временного пароля при первом входе — вход с временным паролём ведёт на экран принудительной смены; доступ в приложение закрыт серверным гейтом до установки постоянного пароля. Флаг must_change_password и механика гейта — модель данных.
Смена email администратором
Администратор меняет email сотрудника из карточки пользователя — на доверии к роли, без письма-подтверждения. Новый адрес проходит lower()-нормализацию и проверку уникальности (адрес занят → 409 CONFLICT). Все refresh-токены пользователя гасятся — он войдёт заново уже по новому адресу, а уже выданный access-токен доживает своё ≤15-минутное окно. Действие пишется в audit_log (action user.email_change, прежний → новый адрес в meta). Гейт — модалка-подтверждение (re-auth — v2). Смена email пересматривает identity mapping (v2).
Сброс «забыл пароль» → Восстановление пароля
Процедура через email. Одноразовый токен (1 ч); повторный запрос гасит прежний — активен только последний, а истёкший и использованный для клиента неотличимы. Refresh-токены гасятся при установке нового пароля. Требует настроенного SMTP — без него ссылки «забыли пароль?» на экране входа нет.
Защита последнего Owner
Запрет удаления / деактивации / понижения единственного активного Owner. Guard на уровне бизнес-логики.
Верификация email
Email Owner принимается на доверии; приглашённые верифицируются доставкой — invite-ссылка уходит только письмом на приглашённый адрес. Сам пользователь email не меняет — это логин-идентификатор. Смену email сотрудника выполняет администратор из карточки пользователя, на доверии к роли (без письма-подтверждения); refresh-токены гасятся, действие пишется в audit log.
3 Проверка пароля → Экран входа Подтверждаем личность.
Email + пароль
Хэширование — argon2id, параметры стойкости заданы в крипто-ядре.
Модель User — готовность к SSO
Схема готова к SSO с самого начала: пароль необязателен, провайдер входа в учётной записи — подключение Okta / Azure AD без миграции. Вход — сменная стратегия (local → IdP). Поля — модель данных.
SSO / OIDCv2
SAML / OIDC (Okta, Azure AD, Google Workspace). Автопровижининг пользователей из IdP. Обмен редиректами с провайдером защищён от подделки входа CSRF.
защита редиректа
state — случайная метка запроса, сверяется при возврате от IdP; не совпала — вход отклоняется.
PKCE — привязывает код авторизации к инициатору входа, чтобы перехваченный код нельзя было обменять (важно для браузерного расширения).
nonce — связывает выданный ID-токен с конкретным запросом входа.
TOTP, допуск ±1 шаг (±30 c) на дрейф часов. Принятый код одноразов: сервер хранит последний использованный шаг времени и отклоняет повтор кода в окне допуска (RFC 6238 §5.2). Обязательность — настройка организации: по умолчанию обязательно для Owner / Admin, опционально для Member. Recovery codes (одноразовые).
хранение секрета
mfa_secret хранится зашифрованным — шифр и работа с ключом в крипто-ядре.
Парольная политика — NIST 800-63B → Admin Panel
Единый свод проверок — источник правды для регистрации, invite, смены и сброса пароля. Все условия должны пройти:
8 ≤ len ≤ 128 Длина — единственное ограничение размера
zxcvbn ≥ 3 Сложность — оценка стойкости 0–4; ниже 3 — отклонить
не в HIBP Утечки — сверка по базе скомпрометированных паролей, k-anonymity (передаём только хэш-префикс). HIBP недоступен → проверка пропускается (fail open), длина и zxcvbn действуют, в журнал пишется warning
composition rules (обязат. цифра / символ) принудительная ротация
Смена пароля POST /api/v1/auth/password/change → Экран профиля
Требует валидный access token.
current + new argon2 verify текущего валидация нового по своду проверок хэш UPDATE user инвалидация всех refresh tokens кроме текущей сессии.
Новый ≠ текущий — отклонение. Аудит: запись в audit log при успехе и неудаче. Перебор: неверный current учитывается per-account счётчиком рубежа перебора (задержка + alert) — украденный access-токен не превращает endpoint в инструмент подбора пароля.
4 Токен → Admin Panel → Cache & Workers Чем подтверждаем уже выполненный вход.
Access + Refresh tokens
Access — stateless JWT, 15 мин.
Refresh — 30 дней sliding, абсолютный потолок 90 дней.
HS256, один SECRET_KEY; верификация с жёстко заданным algorithms=["HS256"] (защита от alg substitution).
В header — kid на активный ключ: ключ сейчас один, но заложен сразу, чтобы будущая ротация не инвалидировала уже выданные токены.
JWT claims
sub (user_id) · role · exp · iat · jti (логирование и отзыв) · iss ("achilles") · aud ("achilles-api").
свежесть claim'ов
role в токене — снимок на момент выпуска; понижение роли и деактивация действуют после обновления access-токена (общее 15-минутное окно stateless-модели). Критичные проверки (управление пользователями, настройки безопасности) читают роль и статус из БД, а не из claim.
5 Сессия Срок действия, обновление и хранение токена.
«Запомнить меня» → Вход
Чекбокс управляет временем жизни cookie, не токена:
  • Снят → session cookie (без Max-Age), удаляется при закрытии браузера (общие машины).
  • Установлен → persistent cookie с Max-Age = TTL refresh-токена (30 д).
Refresh-токен в БД в обоих случаях действует по своему TTL.
Refresh token rotation
При каждом refresh — новый токен, старый инвалидируется. Grace-период ~10 с: только что ротированный токен в этом окне принимается повторно и возвращает тот же новый — доброкачественная гонка вкладок безвредна. Предъявление старого токена за пределами окна — reuse detection → аннулировать всю цепочку (family), не все сессии пользователя.
Access token в SPA
Хранится в памяти JS (не localStorage) — защита от XSS. Передача — Authorization: Bearer. При перезагрузке восстанавливается через /refresh.
Refresh queue — concurrent 401
Один общий /refresh на все запросы, упёршиеся в истёкший токен. Interceptor держит Promise текущего refresh (или null):
  • 401, а refresh уже идёт → ждём тот же Promise.
  • 401, refresh не идёт → запускаем его, сохраняем Promise.
  • Успех → все ждавшие повторяют запрос с новым токеном.
  • Провал → всех отклоняем, logout.
Защиты:
  • Повтор ровно один раз — флаг _retry на запросе. 401 на уже повторённом → сразу logout, без нового круга refresh (защита от зацикливания).
  • Refresh запускает только 401 по истёкшему access token. 401 от самого /refresh → refresh-токен мёртв → logout без рекурсии; 401 по правам пропускается без refresh.
  • Таймаут на Promise refresh — провисший запрос отклоняет всех ждущих и ведёт в logout, а не держит их бесконечно.
  • Между вкладками одноразовый refresh-токен может уйти дважды. Гонку гасит grace-период ротации ↑ Refresh token rotation: опоздавшая вкладка получает тот же новый токен и продолжает работу.
Параллельные сессии
Сессия = одна token family — один вход с устройства или браузера; активная сессия — family с живым refresh-токеном. Число параллельных сессий не ограничено; видимость и управление зависят от роли:
  • Пользователь — свои сессии, самозащита. Видит все свои сессии и завершает любую, кроме текущей, — по одной или «все остальные» (защита при компрометации). Карточка несёт устройство, IP и последнюю активность (из refresh-токена). Экран — управление сессиями.
  • Админ — чужие сессии, без приватных деталей. Устройство, IP и гео владельца админу не показываются (это приватные данные, для реагирования не нужны): в карточке пользователя только счётчик активных сессий и грубое «завершить все». Оно удаляет все refresh-токены — новые access не выдаются, уже выданный stateless access-JWT доживает своё 15-минутное окно (↑ JWT claims). Когда подозрительная сессия известна, а владелец нет, — пользователя называет audit log (auth-события несут session id), дальше — та же карточка.
что добавит v2
Лимит сессий per-user (default 5, настраивается в Admin Panel; при превышении самая старая завершается автоматически).
Оповещение о входе с нового устройства / гео — баннер в UI плюс письмо.
Мгновенный отзыв access-токена через jti-blacklist в Redis — без ожидания 15-минутного окна.
Инвалидация при деактивацииv2
Отзыв refresh tokens + jti access token в Redis blacklist (TTL = lifetime access token).
Повторная аутентификация — sudov2 → Re-auth modal
  • Критичные действия (смена пароля, управление пользователями, настройки безопасности, завершение сессий) требуют повторного ввода пароля — даже в рамках открытой сессии.
  • С включённым MFA проверка принимает TOTP-код.
  • После успеха — grace-period (несколько минут, привязан к текущей сессии): соседние критичные действия проходят без повторного запроса, затем проверка снова закрывается.
  • Неверный ввод считается под тем же rate-limit, что и вход.
⎋ Завершение сессии → Session management
Стандартный: удалить refresh token из БД (опознан по refresh-cookie) + очистить httpOnly cookie. Access token истекает сам (15 мин). Кнопка «Выйти» — хедер админки. Все устройства: удалить все refresh tokens пользователя — сам пользователь, из UI управления сессиями. Принудительный: то же «все устройства», но админом — из карточки пользователя. Удаляются все refresh-токены пользователя; новые access не выдаются, уже выданный stateless access-JWT доживает своё ≤15-минутное окно.
6 Жизненный цикл и ACL → Admin Panel user_id → роль → права и состояние учётной записи.
Статусы
active (по умолчанию после регистрации)
deactivated (отключён администратором).
Поле — в модель данных.
Деактивация — кто может
Owner → любого (кроме единственного Owner). Admin → только Member. Self-деактивация запрещена.
Деактивация — что происходит
Все refresh tokens удаляются, все API-ключи пользователя отзываются (машинный доступ гаснет мгновенно). Access-токен stateless и доживает до expiry — это общее 15-минутное окно stateless-модели. Personal-агенты владельца гасятся (enabled=false), чтобы фоновые прогоны не жгли бюджет под учёткой ушедшего — Agent Engine.
v2
jti в Redis blacklist для мгновенной инвалидации access-токена.
Реактивация
Те же права, что и на деактивацию. Пользователь может снова войти. Токены, API-ключи и агенты не восстанавливаются — нужны новые, агентов владелец включает сам.
Удаление
Удаление — это hard delete: физическое стирание строки, только Owner, необратимо. Soft delete (пометка-флаг с сохранением строки) сознательно не вводим — его роль уже играет деактивация, отдельный механизм состояния учётки. Каскад сносит только auth-данные пользователя (токены, ключи); контент источников и audit_log не затрагиваются.
user_id роль ACL · доступ выдан
API-ключи
боковой вход → MCP → Public API → Admin Panel → HTTP API
key → user_id → роль / ACL
Хранение
Ключ показывается один раз — при создании. В базе остаётся только его SHA-256-хэш и префикс ach_xxxx.
Привязка и ограничение области
Ключ → user_id → роль и ACL пользователя; шире прав владельца ключ не бывает — даже выданный администратором.
детали
Скоуп сужается по двум осям: доступ — только чтение (запись — территория Agent Engine, не ключа); источники — по умолчанию все доступные владельцу, при желании уже. У пользователя может быть несколько ключей.
Срок и ротация
Срок жизни — 30 / 90 / 365 дней или бессрочно; по истечении ключ перестаёт действовать сам, отзывать не нужно. Напоминание об истечении за 7 дней.
ротация
Создать новый → проверить → отозвать старый.
Создание и отзыв
Пользователь создаёт и отзывает свои ключи сам — в профиле или через CLI-вход. Owner и Admin видят ключи всех и могут отозвать любой; отзыв применяется мгновенно через Admin Panel. При деактивации пользователя все его ключи отзываются автоматически.
Выдача администратором · поиск
При выдаче ключа на сотрудника Owner / Admin выбирает пользователя поиском-подсказками. Запрос уходит на бэкенд от 2-го введённого символа и с дебаунсом; сервер возвращает топ 10 совпадений по имени и email.
Rate limiting
Ограничение на один ключ — 60 req/min.
сводится к user_id
ключ сводится к user_id → дальше обычный путь: роль → ACL
OAuthv2
боковой вход → MCP
вход → токен → user_id → роль / ACL
Назначение
Интерактивный вход для клиентов рядом с пользователем: ИИ-ассистент, IDE, CLI. Подключение без ручного переноса ключа.
Поток входа
Клиент открывает страницу входа, пользователь подтверждает доступ — клиент получает токен и хранит его сам.
детали
achilles login открывает браузер; после согласия токен сохраняется локально, дальше клиент работает без участия пользователя. Протокол — OAuth 2.1, как описывает спецификация MCP.
Токены → Токены
Короткоживущий access-токен и продление по refresh — общий механизм сессий.
OAuth или ключ
OAuth — для интерактивных клиентов; API-ключ — для headless (CI, сервер, фоновые задачи), где подтвердить вход некому.
сводится к user_id
токен сводится к user_id → дальше обычный путь: роль → ACL