Skip to content

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

Две независимые системы аутентификации с общей механикой сессий:

Система Субъект Вход Scope в JWT
Админка admins email + пароль, OAuth admin
Кабинет clients Magic link, email + пароль, OAuth client

Токен одного scope не подходит к роутам другого — проверяется middleware до любой бизнес-логики.

Обе системы выдают пару токенов:

  • Access — JWT, 15 минут, содержит sub, scope, role (для админов). Не отзывается — только истекает.
  • Refresh — случайные 32 байта, 30 дней; в БД хранится SHA-256 (sessions.token_hash).

POST …/auth/refresh ротирует пару: старый refresh помечается использованным. Повторное предъявление уже использованного refresh — признак компрометации: отзываются все сессии субъекта. Logout отзывает текущую сессию.

Пока в системе нет ни одного админа, backend работает в bootstrap-режиме:

  1. На старте генерируется одноразовый setup-токен и печатается в лог процесса. Токен живёт до появления первого админа; при рестарте пустой системы генерируется заново.
  2. GET /api/v1/auth/methods возвращает setup_required: true — UI показывает экран первичной настройки вместо логина.
  3. 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 без браузера / headless
astral admin reset-password --email ... # единственный owner потерял доступ

Argon2id (admins.password_hash). password_hash nullable — админ может существовать только с OAuth-входом (пароль владельца, созданного при bootstrap, при этом остаётся рабочим способом входа, если не выключен политикой).

  1. GET /api/v1/auth/oauth/{provider}/redirect → consent screen провайдера.
  2. Callback: код обменивается на профиль.
  3. Есть admin_oauth_identities с этим provider + provider_user_id → вход.
  4. Нет привязки, но email совпадает с существующим админом → автопривязка только при email_verified: true от провайдера; иначе вход отклоняется (защита от перехвата аккаунта через чужой OAuth с совпадающим email). Привязку в этом случае делает сам админ из настроек, будучи залогиненным.
  5. Самостоятельная регистрация новых админов через OAuth закрыта: админов создаёт существующий админ с ролью owner/admin (первый — при инициализации инсталляции).
Метод Доступность
Magic link Всегда: единственный метод, который нельзя выключить
Пароль Если у клиента задан пароль (при регистрации или в кабинете)
OAuth Провайдер сконфигурирован в portal_auth.oauth

Все методы завершаются одинаково — issueSession(client, id); вход возможен только при email_verified_at IS NOT NULL AND status = active, независимо от метода.

Magic link — один механизм в трёх ролях: беспарольный вход; подтверждение email (первое использование проставляет email_verified_at — контроль над ящиком доказан); восстановление доступа при забытом пароле (вход по ссылке → смена пароля в кабинете). Отдельных флоу верификации и сброса пароля не существует.

  1. POST /portal/v1/auth/magic-link {email} — если клиент существует, на email уходит ссылка с одноразовым токеном (TTL 15 минут; в БД — только хэш). Ответ одинаков независимо от существования email — перечисление клиентов невозможно.
  2. POST /portal/v1/auth/callback {token} — проверка хэша и used_at, выдача пары JWT со scope client.

ПарольPOST /portal/v1/auth/login {email, password}, Argon2id (clients.password_hash, nullable).

OAuth — тот же адаптерный флоу и те же правила привязки, что у админки (автопривязка к существующему email — только при email_verified: true от провайдера), но по client_oauth_identities и с отдельной конфигурацией: у кабинета свои redirect URI и свой радиус поражения; тот же client_id провайдера использовать можно, но не обязательно.

Режим — 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 уходит подписчику уведомлений (доменные события) — админ узнаёт о заявке без поллинга.

Клиент может существовать без подписок: зарегистрировался, но ещё не купил и не получил доступ. Покупка требует активной сессии клиента — анонимного чекаута нет, «покупка без регистрации» = регистрация + чекаут.

Роль Права
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 на регистрации; не проектируется до потребности

login, magic-link, register, refresh — лимиты по IP и по субъекту (скользящее окно в PostgreSQL). Ошибки аутентификации и попытки регистрации пишутся в audit_log.