Аутентификация
Две независимые системы аутентификации с общей механикой сессий:
| Система | Субъект | Вход | Scope в JWT |
|---|---|---|---|
| Админка | admins |
email + пароль, OAuth | admin |
| Кабинет | clients |
Magic link, email + пароль, OAuth | client |
Токен одного scope не подходит к роутам другого — проверяется middleware до любой бизнес-логики.
Сессии
Section titled “Сессии”Обе системы выдают пару токенов:
- Access — JWT, 15 минут, содержит
sub,scope,role(для админов). Не отзывается — только истекает. - Refresh — случайные 32 байта, 30 дней; в БД хранится SHA-256 (
sessions.token_hash).
POST …/auth/refresh ротирует пару: старый refresh помечается использованным. Повторное предъявление уже использованного refresh — признак компрометации: отзываются все сессии субъекта. Logout отзывает текущую сессию.
Первый администратор
Section titled “Первый администратор”Пока в системе нет ни одного админа, backend работает в bootstrap-режиме:
- На старте генерируется одноразовый setup-токен и печатается в лог процесса. Токен живёт до появления первого админа; при рестарте пустой системы генерируется заново.
GET /api/v1/auth/methodsвозвращаетsetup_required: true— UI показывает экран первичной настройки вместо логина.POST /api/v1/setup {token, email, password}создаёт первого админа с рольюownerи завершает bootstrap: эндпоинт начинает отвечать404, токен обесценивается. Событие пишется вaudit_log(actor_type: system).
Доступ к логам сервера — доказательство роли оператора: свежеразвёрнутая панель, доступная из интернета, не захватывается без него. Отклонённые варианты: дефолтные креденшелы (не меняют), пароль в env (оседает в .env и истории деплоя навсегда), «первый вошедший становится owner» (гонка с внешним миром).
Fallback и восстановление — CLI на сервере панели, напрямую в БД, минуя HTTP:
astral admin create --email ... --role owner # bootstrap без браузера / headlessastral admin reset-password --email ... # единственный owner потерял доступАдминка: пароль
Section titled “Админка: пароль”Argon2id (admins.password_hash). password_hash nullable — админ может существовать только с OAuth-входом (пароль владельца, созданного при bootstrap, при этом остаётся рабочим способом входа, если не выключен политикой).
Админка: OAuth
Section titled “Админка: OAuth”GET /api/v1/auth/oauth/{provider}/redirect→ consent screen провайдера.- Callback: код обменивается на профиль.
- Есть
admin_oauth_identitiesс этимprovider + provider_user_id→ вход. - Нет привязки, но email совпадает с существующим админом → автопривязка только при
email_verified: trueот провайдера; иначе вход отклоняется (защита от перехвата аккаунта через чужой OAuth с совпадающим email). Привязку в этом случае делает сам админ из настроек, будучи залогиненным. - Самостоятельная регистрация новых админов через OAuth закрыта: админов создаёт существующий админ с ролью
owner/admin(первый — при инициализации инсталляции).
Кабинет: методы входа
Section titled “Кабинет: методы входа”| Метод | Доступность |
|---|---|
| Magic link | Всегда: единственный метод, который нельзя выключить |
| Пароль | Если у клиента задан пароль (при регистрации или в кабинете) |
| OAuth | Провайдер сконфигурирован в portal_auth.oauth |
Все методы завершаются одинаково — issueSession(client, id); вход возможен только при email_verified_at IS NOT NULL AND status = active, независимо от метода.
Magic link — один механизм в трёх ролях: беспарольный вход; подтверждение email (первое использование проставляет email_verified_at — контроль над ящиком доказан); восстановление доступа при забытом пароле (вход по ссылке → смена пароля в кабинете). Отдельных флоу верификации и сброса пароля не существует.
POST /portal/v1/auth/magic-link {email}— если клиент существует, на email уходит ссылка с одноразовым токеном (TTL 15 минут; в БД — только хэш). Ответ одинаков независимо от существования email — перечисление клиентов невозможно.POST /portal/v1/auth/callback {token}— проверка хэша иused_at, выдача пары JWT со scopeclient.
Пароль — POST /portal/v1/auth/login {email, password}, Argon2id (clients.password_hash, nullable).
OAuth — тот же адаптерный флоу и те же правила привязки, что у админки (автопривязка к существующему email — только при email_verified: true от провайдера), но по client_oauth_identities и с отдельной конфигурацией: у кабинета свои redirect URI и свой радиус поражения; тот же client_id провайдера использовать можно, но не обязательно.
Саморегистрация
Section titled “Саморегистрация”Режим — portal_auth.registration:
| Режим | Поведение |
|---|---|
closed (default) |
Эндпоинты регистрации отвечают 404; клиентов создаёт только админ |
application |
Регистрация создаёт клиента в статусе pending; вход — после одобрения админом |
open |
Клиент активен сразу после подтверждения email |
Статусы клиента: pending → active (одобрение), pending → rejected (отказ), active ↔ disabled (бан/разбан). Созданные админом — сразу active с подтверждённым email (админ ручается за адрес).
- Регистрация паролем: создаётся клиент с
email_verified_at NULL, отправляется magic link как письмо подтверждения. Неподтверждённые записи удаляются фоновой задачей через 24 ч. - Регистрация через OAuth:
email_verified: trueот провайдера засчитывается как подтверждение; вopenклиент активен немедленно. - Повторная регистрация на существующий email —
409, в том числе дляrejected(повторную попытку разблокирует админ, удалив или одобрив запись). - В режиме
applicationсобытиеclient.registeredуходит подписчику уведомлений (доменные события) — админ узнаёт о заявке без поллинга.
Клиент может существовать без подписок: зарегистрировался, но ещё не купил и не получил доступ. Покупка требует активной сессии клиента — анонимного чекаута нет, «покупка без регистрации» = регистрация + чекаут.
Роли админов
Section titled “Роли админов”| Роль | Права |
|---|---|
owner |
Всё + управление админами и настройками инсталляции |
admin |
Всё, кроме управления админами |
viewer |
Только чтение |
Роль зашита в access-токен; проверка — декларативно на роуте (requireRole(...)). Более гранулярные права не вводятся, пока нет практической необходимости.
Политика аутентификации
Section titled “Политика аутентификации”Какие методы входа включены — статическая конфигурация backend, не данные:
auth: # админка password_login: true # ASTRAL_AUTH_PASSWORD_LOGIN oauth: # провайдер сконфигурирован ⇒ включён google: client_id: ... client_secret: ... # секреты — только через env
portal_auth: # кабинет registration: closed # closed | application | open (ASTRAL_PORTAL_REGISTRATION) password_login: true oauth: google: { client_id: ..., client_secret: ... }- Отдельного флага «включён» у OAuth-провайдера нет: наличие конфигурации и есть включение — флаг рядом со списком был бы вторым источником истины.
- Конфигурация, а не БД: смена методов входа и режима регистрации — операционное решение уровня деплоя; хранение в БД добавляет UI настроек, кэш и риск самолокаута из админки.
- Роуты выключенного метода отвечают
404. Формы логина/регистрации строятся поGET /api/v1/auth/methodsиGET /portal/v1/auth/methods(без аутентификации: включённые методы, для кабинета — и режим регистрации). - Magic link кабинета политикой не управляется: это механизм подтверждения email и восстановления доступа, выключать его нечем.
Инварианты при старте (ошибка конфигурации — backend не стартует):
auth.password_login: falseбез единого OAuth-провайдера админки;portal_auth.registration != closedприpassword_login: trueбез настроенного SMTP — некому отправлять письма подтверждения.
Расширение: новые методы входа
Section titled “Расширение: новые методы входа”Все методы сходятся в issueSession(subject_type, subject_id) — верификация способа входа отделена от выдачи сессии, поэтому новый метод — это новый верификатор и роут, без изменений в сессиях, refresh-ротации и RBAC.
OAuth-провайдер — адаптер (тот же паттерн, что платёжные провайдеры):
type OAuthProvider interface { AuthURL(state string) string Exchange(ctx context.Context, code string) (Profile, error)}
type Profile struct { ProviderUserID string Email string EmailVerified bool}Реестр адаптеров заполняется из конфигурации; резолв субъекта по admin_oauth_identities и правила привязки (см. выше) — общие для всех провайдеров. Добавление Google/GitHub — адаптер + секция конфига, core не меняется.
| Слой | Состав |
|---|---|
| Core | Пароль (Argon2id), сессии с ротацией refresh, роли админов, magic link клиентов |
| Optional | OAuth-провайдеры обеих поверхностей (адаптеры, включаются конфигурацией), отключение парольного входа, саморегистрация клиентов |
| Future | TOTP/WebAuthn как второй фактор — ещё один шаг верификации перед issueSession; CAPTCHA на регистрации; не проектируется до потребности |
Rate limiting
Section titled “Rate limiting”login, magic-link, register, refresh — лимиты по IP и по субъекту (скользящее окно в PostgreSQL). Ошибки аутентификации и попытки регистрации пишутся в audit_log.