Задача: Odoo дозволяє ввімкнути двофакторну автентифікацію, але не вимагає її, а форма
реєстрації /web/signup за замовчуванням відкрита будь-якому боту. Обидва питання
закривають два невеликі модулі — kw_2fa і kw_auth_signup_protection. Розглянемо, як
саме, і де це працює умовно.
Де Odoo перевіряє 2FA
Після пароля Session.authenticate (odoo/http.py) вирішує, чи потрібен другий фактор:
user = env['res.users'].browse(pre_uid)
if auth_info.get('mfa') == 'skip' or not user._mfa_url():
self.finalize(env)
Якщо _mfa_url() дає None — сесія одразу фіналізується. Стандартний auth_totp
повертає URL лише тоді, коли TOTP вже увімкнено самим користувачем; поки ні — None,
і другого фактора просто не буде. 2FA в Odoo з коробки — опція, не вимога.
kw_2fa: як зроблено примус
Модуль перевизначає _mfa_type, і для звичайного користувача він завжди повертає 'totp':
def _mfa_type(self):
if self.id == self.env.ref('base.user_root').id:
return super()._mfa_type()
return 'totp'
def _mfa_url(self):
if not self.totp_enabled:
return '/kw_2fa/setup_totp'
return super()._mfa_url()
Тепер _mfa_url() для звичайного користувача ніколи не дає None. Без налаштованого
TOTP людина після вірного пароля потрапляє не в Odoo, а на /kw_2fa/setup_totp —
QR-код і поле коду, без кнопки «пропустити». Сесія лишається частковою, поки код не
введено вірно.
Важливо! З примусу виключено лише base.user_root — технічний акаунт id=1
(«OdooBot»), яким Odoo сама користується для крону й оновлень модулів, а не той, під
яким ви логінитесь щодня. Звичайний admin (id=2) під примус потрапляє так само, як
і всі інші.
Адмін загубив пристрій
kw_2fa не чіпає штатне скидання. Будь-який адміністратор може відкрити картку
заблокованого колеги (Налаштування → Користувачі → вкладка Account Security) і
вимкнути йому 2FA тумблером — це action_totp_disable, захищений повторним введенням
власного пароля (check_identity):
@check_identity
def action_totp_disable(self):
if not (self == self.env.user or self.env.user._is_admin() or self.env.su):
return False
self.revoke_all_devices()
self.sudo().write({'totp_secret': False})
Після цього totp_enabled знову False, і при наступному вході kw_2fa відправить
користувача на /kw_2fa/setup_totp заново — з новим секретом. Якщо ж пристрій
втратив єдиний адмін і зайти нема кому — лишається пряме
UPDATE res_users SET totp_secret = NULL WHERE id = X;: totp_secret — звичайна
колонка res_users, без додаткових перевірок цілісності на запис.
2FA і сервісні акаунти
Тут неочевидний побічний ефект самого Odoo, не kw_2fa: щойно в акаунта увімкнено TOTP,
він втрачає право на password-автентифікацію по XML-RPC — _rpc_api_keys_only()
стає True, і _check_credentials при interactive=False приймає замість пароля
лише res.users.apikeys. Оскільки kw_2fa рано чи пізно доводить TOTP до кожного
акаунта, це зачіпає й наші власні інтеграції: kw_api видає токен саме через цей
виклик:
user.with_user(user)._check_credentials(
credential, {'interactive': False})
Неправильно — тримати пароль сервісного акаунта в конфігу деплою й дивуватись,
чому інтеграція відвалилась одразу після того, як цей акаунт пройшов примусову 2FA.
Правильно — видати сервісному акаунту окремий API-ключ (Налаштування →
Користувачі → API Keys) і підставляти саме його в поле password — тоді 2FA на
інтеграцію взагалі не впливає.
kw_auth_signup_protection: чотири механізми, один із них активний одразу
Перевірки йдуть по черзі, кожна — за окремим чекбоксом у Налаштуваннях. Тут є нюанс: honeypot працює одразу після встановлення модуля, три інші вмикає вручну сам адмін:
if get_param('kw_auth_signup_protection.honeypot_enabled') == 'True':
if request.params.get('website_url'):
return _('Signup failed. Please try again.')
if get_param('kw_auth_signup_protection.recaptcha_enabled') == 'True':
result = ir_http._verify_request_recaptcha_token('signup')
...
if get_param('kw_auth_signup_protection.disposable_email_block') == 'True':
...
- Honeypot (увімкнений за замовчуванням) — прихований
<input name="website_url">(CSSopacity:0; position:absolute, неdisplay:none, щоб не ловитись на перевірку видимості черезdisplay). Заповнене поле — бот, форма мовчки відхиляється. - reCAPTCHA — реалізована через
google_recaptcha, обов'язкову залежність модуля. Важливо! Умовність тут подвійна: увімкненого чекбокса замало — якщо в Загальних налаштуваннях не заповненоrecaptcha_private_key, перевірка тихо повертаєno_secretі завжди проходить, ніби її й немає. Прапорець без ключа — ілюзія захисту, не захист. - Блок одноразових доменів — звірка з
kw.disposable.email.domain, статичним сідом на 108 доменів (noupdate="1"), завантаженим один раз при встановленні й ніколи не оновлюваним автоматично. Поповнювати — вручну, тим самим списком у Налаштуваннях. - Верифікація пошти — окремий прапорець, спрацьовує не разом із трьома іншими,
а після них: замість негайного створення юзера форма зберігає заявку в
kw.pending.signupі шле лист із токеном, акаунт створюється лише по кліку на посилання. Тут же діє рейт-ліміт: не більше 3 заявок на один email і 5 на одну IP-адресу за годину.
Живий дефект: клієнтський reCAPTCHA не запускається на Odoo 18
static/src/js/signup.js підключений як актив web.assets_frontend, але написаний у
синтаксисі odoo.define(name, function (require) {...}) — двоаргументна форма, яку Odoo
прибрала ще у версії 15. Лоадер 18-ї гілки (web/module_loader.js) чекає другим
аргументом масив залежностей; отримавши функцію замість масиву, кидає помилку синхронно,
до того як тіло фабрики взагалі виконається. Шару зворотної сумісності для старого
синтаксису в ядрі немає.
Наслідок подвійний. По-перше, власний JS модуля (_kwInitRecaptcha, підстановка токена в
#recaptcha_token_response) не завантажується. По-друге — і це вже гірше — шаблон
views/auth_signup_templates.xml додає в форму той самий прихований input
recaptcha_token_response. Штатний google_recaptcha перехоплює сабміт лише коли цього
поля в DOM ще немає; тут воно є, тому й core-віджет мовчки нічого не робить. Токен у
результаті не підставляє ніхто.
Важливо! Критичність залежить від конфігурації, і вона не в бік «менше захисту», а в
бік «реєстрація відмовляє всім». Якщо recaptcha_enabled=True і приватний ключ
заповнений — форма щоразу шле порожній токен, Google повертає missing-input-response, і
сигнап падає для будь-кого, боти й реальні люди однаково. Якщо ж ключ не заповнений —
дефект нейтральний: no_secret і без бага пропускав усе. Тестами це не ловиться:
test_honeypot.py працює через HttpCase без браузера, а test_recaptcha.py фізично
відсутній із дерева.
Три інших механізми — honeypot, блок одноразових доменів, рейт-ліміт верифікації
пошти — серверні й від цього JS не залежать, працюють як задумано. Ну ок, тут і висновок:
серверна перевірка переживає зміну версії фронтенду, клієнтська — ні. Якщо плануєте
вмикати reCAPTCHA саме через цей модуль на Odoo 18 — спершу полагодьте signup.js під
ES-модуль, інакше отримаєте або ілюзію захисту, або зупинену реєстрацію.
Чого це не закриває
Honeypot і reCAPTCHA ловлять автоматизованих ботів, які не рендерять форму по-справжньому. Проти цільової атаки — людини з реальною поштою, що вручну заповнює форму, — з чотирьох механізмів працюють тільки верифікація пошти й рейт-ліміт. А форсована 2FA нічого не дає, якщо адміністратор сам роздав колегам спільний логін-пароль до одного акаунта: TOTP-секрет прив'язаний до акаунта, а не до людини, і хтось же показав їй той QR-код при першому вході.