Задача: 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 — виправлено
Оновлено 30.08.2026: дефект нижче виправлено у версії 18.0.1.0.3.
До фіксу static/src/js/signup.js підключався як актив web.assets_frontend, але був
написаний у синтаксисі odoo.define(name, function (require) {...}) — двоаргументній
формі, яку Odoo прибрала ще у версії 15. Лоадер 18-ї гілки (web/module_loader.js) чекає
масив залежностей другим аргументом; отримавши функцію замість масиву, кидав помилку
синхронно, до виконання тіла фабрики. Наслідок був подвійний: власний JS модуля
(_kwInitRecaptcha) не завантажувався, а шаблон auth_signup_templates.xml усе одно
додавав у форму прихований input#recaptcha_token_response — той самий, який
google_recaptcha шукає в DOM, щоб зрозуміти, чи треба перехоплювати сабміт. Побачивши
поле, core-віджет мовчки нічого не робив. При заповненому recaptcha_private_key
реєстрація падала для всіх; при порожньому — дефект лишався непомітним, бо no_secret і
так пропускав усе.
Полагодили не портуванням скрипта під ES-модуль, а видаленням: signup.js прибрали з
дерева й з assets маніфесту цілком, разом із дубльованим input у шаблоні. reCAPTCHA
тепер повністю на боці штатного google_recaptcha / auth_signup (SignupCaptcha) —
дубля поля немає, перехоплювати сабміт є кому. Тести повернулись разом із фіксом:
test_recaptcha.py перевіряє, що recaptcha_token_response у HTML форми зустрічається
нуль разів, і для увімкненого, і для вимкненого ключа (коміт dab54fd, MR !112).
Три інших механізми — honeypot, блок одноразових доменів, рейт-ліміт верифікації пошти — увесь цей час залишались серверними і від зламаного JS не залежали. Ну ок, тут і висновок, і фікс лише підтвердив його на практиці: серверна перевірка пережила зміну версії фронтенду без правок, клієнтська — ні. А другий шар висновку глибший: найдешевшим фіксом виявилось не чинити власний JS, а видалити його на користь того самого core-механізму, який він же і глушив. Клієнтський код, що дублює штатний, — це борг: рано чи пізно за нього платиш або підтримкою на кожен реліз ядра, або видаленням.
Чого це не закриває
Honeypot і reCAPTCHA ловлять автоматизованих ботів, які не рендерять форму по-справжньому. Проти цільової атаки — людини з реальною поштою, що вручну заповнює форму, — з чотирьох механізмів працюють тільки верифікація пошти й рейт-ліміт. А форсована 2FA нічого не дає, якщо адміністратор сам роздав колегам спільний логін-пароль до одного акаунта: TOTP-секрет прив'язаний до акаунта, а не до людини, і хтось же показав їй той QR-код при першому вході.