feat(auth): вход через OAuth-провайдеры (GitHub, Google, Discord)
Фаза 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 — кнопки провайдеров и ошибка возврата; серверная ручка отдаёт понятный отказ без настроек.
This commit is contained in:
@@ -60,6 +60,11 @@ type Service struct {
|
||||
webauthn *webauthn.WebAuthn
|
||||
// ceremonies — начатые церемонии WebAuthn (в памяти, одноразовые).
|
||||
ceremonies *ceremonyStore
|
||||
// oauth — включённые провайдеры (client id/secret из env), oauthRedirectBase —
|
||||
// внешний адрес инстанса для redirect_uri, stateKey — ключ подписи state.
|
||||
oauth map[string]oauthClientCredentials
|
||||
oauthRedirectBase string
|
||||
stateKey string
|
||||
}
|
||||
|
||||
type instanceSettings interface {
|
||||
@@ -108,6 +113,10 @@ func New(ctx context.Context, cfg config.Config, st *store.Store, logger *slog.L
|
||||
sessionTTL = store.SessionTTL
|
||||
}
|
||||
|
||||
// OAuth-провайдеры и RP-конфигурация passkeys: включаются конфигом, при
|
||||
// отсутствии настроек соответствующие ручки отвечают понятной ошибкой.
|
||||
oauthClients, oauthRedirectBase := oauthFromConfig(cfg, cfg.BaseURL())
|
||||
|
||||
service := &Service{
|
||||
store: st,
|
||||
hasher: hasher,
|
||||
@@ -117,7 +126,11 @@ func New(ctx context.Context, cfg config.Config, st *store.Store, logger *slog.L
|
||||
policy: policy,
|
||||
cfg: Config{SessionTTL: sessionTTL, MaxLoginFails: 5, LockoutWindow: 15 * time.Minute},
|
||||
logger: logger,
|
||||
ceremonies: newCeremonyStore(),
|
||||
// state OAuth подписывается секретом сессий: отдельного секрета не нужно.
|
||||
oauth: oauthClients,
|
||||
oauthRedirectBase: oauthRedirectBase,
|
||||
stateKey: cfg.SessionPepper,
|
||||
ceremonies: newCeremonyStore(),
|
||||
}
|
||||
// Passkeys требуют корректного RP ID: для IP-адреса (стенд без домена)
|
||||
// браузеры WebAuthn не работают, поэтому такой инстанс просто живёт без них.
|
||||
|
||||
Reference in New Issue
Block a user