Skip to Content

Примусова 2FA та захист форми реєстрації в Odoo

Задача: 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':
    ...
  1. Honeypot (увімкнений за замовчуванням) — прихований <input name="website_url"> (CSS opacity:0; position:absolute, не display:none, щоб не ловитись на перевірку видимості через display). Заповнене поле — бот, форма мовчки відхиляється.
  2. reCAPTCHA — реалізована через google_recaptcha, обов'язкову залежність модуля. Важливо! Умовність тут подвійна: увімкненого чекбокса замало — якщо в Загальних налаштуваннях не заповнено recaptcha_private_key, перевірка тихо повертає no_secret і завжди проходить, ніби її й немає. Прапорець без ключа — ілюзія захисту, не захист.
  3. Блок одноразових доменів — звірка з kw.disposable.email.domain, статичним сідом на 108 доменів (noupdate="1"), завантаженим один раз при встановленні й ніколи не оновлюваним автоматично. Поповнювати — вручну, тим самим списком у Налаштуваннях.
  4. Верифікація пошти — окремий прапорець, спрацьовує не разом із трьома іншими, а після них: замість негайного створення юзера форма зберігає заявку в 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-код при першому вході.

Примусова 2FA та захист форми реєстрації в Odoo
KitWorks, Volodymyr Karabanov 13 серпня 2026 р.
Поділитися цією публікацією
Теги
Архів