Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ jobs:
name: test with OCB
makepot: "true"
- container: ghcr.io/oca/oca-ci/py3.10-odoo18.0:latest
exclude: "password_security"
include: "auth_totp_enforce"
name: test with Odoo
- container: ghcr.io/oca/oca-ci/py3.10-ocb18.0:latest
exclude: "password_security"
include: "auth_totp_enforce"
name: test with OCB
makepot: "true"
- container: ghcr.io/oca/oca-ci/py3.10-odoo18.0:latest
exclude: "password_security,auth_totp_enforce"
name: test with Odoo
- container: ghcr.io/oca/oca-ci/py3.10-ocb18.0:latest
exclude: "password_security,auth_totp_enforce"
name: test with OCB
makepot: "true"
services:
Expand Down
100 changes: 100 additions & 0 deletions auth_totp_enforce/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
=================================
Enforce Two-Factor Authentication
=================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:d7a9647142270eff465347946c269fc042be4388b9f9d22a0d3ff1239f105dae
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
:target: https://github.com/OCA/server-auth/tree/18.0/auth_totp_enforce
:alt: OCA/server-auth
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-auth_totp_enforce
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module forces internal users to configure two-factor authentication
(a TOTP authenticator app) before they log in.

Enforcement works like the password expiry flow of
``password_security``: nobody is logged out when the module is
installed, but on their next login users are held in a mandatory step
and cannot reach the backend until they have enabled an authenticator
app.

Members of the **Exempt from 2FA enforcement** group are never forced.
Portal and public users are out of scope at the moment.

**Table of contents**

.. contents::
:local:

Usage
=====

Once installed, any enforced internal user without an authenticator app
configured will, on their next login:

1. Enter their login and password as usual.
2. Be redirected to a mandatory setup page showing a QR code and secret
key.
3. Scan the code with an authenticator app and enter the generated code.
4. Be handed over to the standard two-factor step to complete the login.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-auth/issues/new?body=module:%20auth_totp_enforce%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* ForgeFlow

Contributors
------------

- ForgeFlow S.L. <contact@forgeflow.com>

- Laura Cazorla <laura.cazorla@forgeflow.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/18.0/auth_totp_enforce>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
5 changes: 5 additions & 0 deletions auth_totp_enforce/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import models
from . import controllers
18 changes: 18 additions & 0 deletions auth_totp_enforce/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Enforce Two-Factor Authentication",
"summary": "Force users to set up an authenticator app before they can log in.",
"version": "18.0.1.0.0",
"author": "ForgeFlow, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/server-auth",
"category": "Authentication",
"license": "AGPL-3",
"depends": ["auth_totp"],
"data": [
"security/res_groups.xml",
"views/templates.xml",
],
"installable": True,
}
4 changes: 4 additions & 0 deletions auth_totp_enforce/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from . import home
71 changes: 71 additions & 0 deletions auth_totp_enforce/controllers/home.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import _, http
from odoo.http import request

import odoo.addons.auth_totp.controllers.home

SESSION_KEY = "totp_enforce_setup"


class Home(odoo.addons.auth_totp.controllers.home.Home):
@http.route(
"/web/login/totp/setup",
type="http",
auth="public",
methods=["GET", "POST"],
sitemap=False,
website=True,
multilang=False,
)
def web_totp_setup(self, redirect=None, **kwargs):
if request.session.uid:
return request.redirect(
self._login_redirect(request.session.uid, redirect=redirect)
)
if not request.session.pre_uid:
return request.redirect("/web/login")
user = request.env["res.users"].sudo().browse(request.session.pre_uid)
if not user._mfa_enforced() or user.totp_enabled:
return request.redirect("/web/login/totp")
stored = request.session.get(SESSION_KEY)
if not stored or stored.get("uid") != user.id:
secret = user._generate_totp_setup_secret()
request.session[SESSION_KEY] = {"uid": user.id, "secret": secret}
else:
secret = stored["secret"]
error = None
if request.httprequest.method == "POST" and kwargs.get("totp_token"):
if user._totp_enforce_setup(secret, kwargs["totp_token"]):
request.session.pop(SESSION_KEY, None)
# Persist secret, session stays partial and delegate to standard MFA
request.env.flush_all()
user.invalidate_recordset(["totp_secret", "totp_enabled"])
request.session.touch()
return request.redirect(
self._login_redirect(user.id, redirect=redirect)
)
error = _("Verification failed, please double-check the 6-digit code")
wizard = (
request.env["auth_totp.wizard"]
.sudo()
.new(
{
"user_id": user.id,
"secret": secret,
}
)
)
request.session.touch()
return request.render(
"auth_totp_enforce.auth_totp_setup_form",
{
"user": user,
"error": error,
"redirect": redirect,
"secret": secret,
"qrcode": wizard.qrcode,
"url": wizard.url,
},
)
4 changes: 4 additions & 0 deletions auth_totp_enforce/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import res_users
57 changes: 57 additions & 0 deletions auth_totp_enforce/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright 2026 ForgeFlow S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

import base64
import functools
import logging
import os
import re

from odoo import models
from odoo.http import request

from odoo.addons.auth_totp.models.totp import TOTP, TOTP_SECRET_SIZE

_logger = logging.getLogger(__name__)

compress = functools.partial(re.sub, r"\s", "")


class ResUsers(models.Model):
_inherit = "res.users"

def _mfa_enforced(self):
self.ensure_one()
user = self.sudo()
exempt_group = "auth_totp_enforce.group_mfa_exempt"
return user._is_internal() and not user.has_group(exempt_group)

def _mfa_url(self):
r = super()._mfa_url()
if r is not None:
return r
if self._mfa_enforced() and not self.totp_enabled:
return "/web/login/totp/setup"

def _generate_totp_setup_secret(self):
secret = base64.b32encode(os.urandom(TOTP_SECRET_SIZE // 8)).decode()
return " ".join(map("".join, zip(*[iter(secret)] * 4, strict=False)))

def _totp_enforce_setup(self, secret, code):
self.ensure_one()
assert self.env.su or (
request
and not request.session.uid
and request.session.get("pre_uid") == self.id
), "Only callable for the user currently in the pre-authentication phase"
secret = compress(secret).upper()
try:
code = int(compress(str(code)))
except ValueError:
return False
if TOTP(base64.b32decode(secret)).match(code) is None:
_logger.info("2FA enforce setup: REJECT CODE for %r", self.login)
return False
self.sudo().totp_secret = secret
_logger.info("2FA enforce setup: SUCCESS for %r", self.login)
return True
3 changes: 3 additions & 0 deletions auth_totp_enforce/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
2 changes: 2 additions & 0 deletions auth_totp_enforce/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- ForgeFlow S.L. \<<contact@forgeflow.com>\>
- Laura Cazorla \<<laura.cazorla@forgeflow.com>\>
10 changes: 10 additions & 0 deletions auth_totp_enforce/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This module forces internal users to configure two-factor authentication
(a TOTP authenticator app) before they log in.

Enforcement works like the password expiry flow of `password_security`: nobody
is logged out when the module is installed, but on their next login users are
held in a mandatory step and cannot reach the backend until they have enabled
an authenticator app.

Members of the **Exempt from 2FA enforcement** group are never forced. Portal
and public users are out of scope at the moment.
7 changes: 7 additions & 0 deletions auth_totp_enforce/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Once installed, any enforced internal user without an authenticator app
configured will, on their next login:

1. Enter their login and password as usual.
2. Be redirected to a mandatory setup page showing a QR code and secret key.
3. Scan the code with an authenticator app and enter the generated code.
4. Be handed over to the standard two-factor step to complete the login.
10 changes: 10 additions & 0 deletions auth_totp_enforce/security/res_groups.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="group_mfa_exempt" model="res.groups">
<field name="name">Exempt from 2FA enforcement</field>
<field name="category_id" ref="base.module_category_hidden" />
<field
name="comment"
>Members of this group are NOT forced to configure MFA at login.</field>
</record>
</odoo>
Loading
Loading