Skip to content

Commit 8c97332

Browse files
[ADD] auth_totp_enforce
1 parent 09f5c08 commit 8c97332

17 files changed

Lines changed: 977 additions & 0 deletions

File tree

auth_totp_enforce/README.rst

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
=================================
2+
Enforce Two-Factor Authentication
3+
=================================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:d7a9647142270eff465347946c269fc042be4388b9f9d22a0d3ff1239f105dae
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github
20+
:target: https://github.com/OCA/server-auth/tree/18.0/auth_totp_enforce
21+
:alt: OCA/server-auth
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-auth_totp_enforce
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module forces internal users to configure two-factor authentication
32+
(a TOTP authenticator app) before they log in.
33+
34+
Enforcement works like the password expiry flow of
35+
``password_security``: nobody is logged out when the module is
36+
installed, but on their next login users are held in a mandatory step
37+
and cannot reach the backend until they have enabled an authenticator
38+
app.
39+
40+
Members of the **Exempt from 2FA enforcement** group are never forced.
41+
Portal and public users are out of scope at the moment.
42+
43+
**Table of contents**
44+
45+
.. contents::
46+
:local:
47+
48+
Usage
49+
=====
50+
51+
Once installed, any enforced internal user without an authenticator app
52+
configured will, on their next login:
53+
54+
1. Enter their login and password as usual.
55+
2. Be redirected to a mandatory setup page showing a QR code and secret
56+
key.
57+
3. Scan the code with an authenticator app and enter the generated code.
58+
4. Be handed over to the standard two-factor step to complete the login.
59+
60+
Bug Tracker
61+
===========
62+
63+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-auth/issues>`_.
64+
In case of trouble, please check there if your issue has already been reported.
65+
If you spotted it first, help us to smash it by providing a detailed and welcomed
66+
`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**>`_.
67+
68+
Do not contact contributors directly about support or help with technical issues.
69+
70+
Credits
71+
=======
72+
73+
Authors
74+
-------
75+
76+
* ForgeFlow
77+
78+
Contributors
79+
------------
80+
81+
- ForgeFlow S.L. <contact@forgeflow.com>
82+
83+
- Laura Cazorla <laura.cazorla@forgeflow.com>
84+
85+
Maintainers
86+
-----------
87+
88+
This module is maintained by the OCA.
89+
90+
.. image:: https://odoo-community.org/logo.png
91+
:alt: Odoo Community Association
92+
:target: https://odoo-community.org
93+
94+
OCA, or the Odoo Community Association, is a nonprofit organization whose
95+
mission is to support the collaborative development of Odoo features and
96+
promote its widespread use.
97+
98+
This module is part of the `OCA/server-auth <https://github.com/OCA/server-auth/tree/18.0/auth_totp_enforce>`_ project on GitHub.
99+
100+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

auth_totp_enforce/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright 2026 ForgeFlow S.L.
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from . import models
5+
from . import controllers

auth_totp_enforce/__manifest__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright 2026 ForgeFlow S.L.
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
{
5+
"name": "Enforce Two-Factor Authentication",
6+
"summary": "Force users to set up an authenticator app before they can log in.",
7+
"version": "18.0.1.0.0",
8+
"author": "ForgeFlow, Odoo Community Association (OCA)",
9+
"website": "https://github.com/OCA/server-auth",
10+
"category": "Authentication",
11+
"license": "AGPL-3",
12+
"depends": ["auth_totp"],
13+
"data": [
14+
"security/res_groups.xml",
15+
"views/templates.xml",
16+
],
17+
"installable": True,
18+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright 2026 ForgeFlow S.L.
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
4+
from . import home
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Copyright 2026 ForgeFlow S.L.
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from odoo import _, http
5+
from odoo.http import request
6+
7+
import odoo.addons.auth_totp.controllers.home
8+
9+
SESSION_KEY = "totp_enforce_setup"
10+
11+
12+
class Home(odoo.addons.auth_totp.controllers.home.Home):
13+
@http.route(
14+
"/web/login/totp/setup",
15+
type="http",
16+
auth="public",
17+
methods=["GET", "POST"],
18+
sitemap=False,
19+
website=True,
20+
multilang=False,
21+
)
22+
def web_totp_setup(self, redirect=None, **kwargs):
23+
if request.session.uid:
24+
return request.redirect(
25+
self._login_redirect(request.session.uid, redirect=redirect)
26+
)
27+
if not request.session.pre_uid:
28+
return request.redirect("/web/login")
29+
user = request.env["res.users"].sudo().browse(request.session.pre_uid)
30+
if not user._mfa_enforced() or user.totp_enabled:
31+
return request.redirect("/web/login/totp")
32+
stored = request.session.get(SESSION_KEY)
33+
if not stored or stored.get("uid") != user.id:
34+
secret = user._generate_totp_setup_secret()
35+
request.session[SESSION_KEY] = {"uid": user.id, "secret": secret}
36+
else:
37+
secret = stored["secret"]
38+
error = None
39+
if request.httprequest.method == "POST" and kwargs.get("totp_token"):
40+
if user._totp_enforce_setup(secret, kwargs["totp_token"]):
41+
request.session.pop(SESSION_KEY, None)
42+
# Persist secret, session stays partial and delegate to standard MFA
43+
request.env.flush_all()
44+
user.invalidate_recordset(["totp_secret", "totp_enabled"])
45+
request.session.touch()
46+
return request.redirect(
47+
self._login_redirect(user.id, redirect=redirect)
48+
)
49+
error = _("Verification failed, please double-check the 6-digit code")
50+
wizard = (
51+
request.env["auth_totp.wizard"]
52+
.sudo()
53+
.new(
54+
{
55+
"user_id": user.id,
56+
"secret": secret,
57+
}
58+
)
59+
)
60+
request.session.touch()
61+
return request.render(
62+
"auth_totp_enforce.auth_totp_setup_form",
63+
{
64+
"user": user,
65+
"error": error,
66+
"redirect": redirect,
67+
"secret": secret,
68+
"qrcode": wizard.qrcode,
69+
"url": wizard.url,
70+
},
71+
)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Copyright 2026 ForgeFlow S.L.
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
from . import res_users
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2026 ForgeFlow S.L.
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
3+
4+
import base64
5+
import functools
6+
import logging
7+
import os
8+
import re
9+
10+
from odoo import models
11+
from odoo.http import request
12+
13+
from odoo.addons.auth_totp.models.totp import TOTP, TOTP_SECRET_SIZE
14+
15+
_logger = logging.getLogger(__name__)
16+
17+
compress = functools.partial(re.sub, r"\s", "")
18+
19+
20+
class ResUsers(models.Model):
21+
_inherit = "res.users"
22+
23+
def _mfa_enforced(self):
24+
self.ensure_one()
25+
user = self.sudo()
26+
exempt_group = "auth_totp_enforce.group_mfa_exempt"
27+
return user._is_internal() and not user.has_group(exempt_group)
28+
29+
def _mfa_url(self):
30+
r = super()._mfa_url()
31+
if r is not None:
32+
return r
33+
if self._mfa_enforced() and not self.totp_enabled:
34+
return "/web/login/totp/setup"
35+
36+
def _generate_totp_setup_secret(self):
37+
secret = base64.b32encode(os.urandom(TOTP_SECRET_SIZE // 8)).decode()
38+
return " ".join(map("".join, zip(*[iter(secret)] * 4, strict=False)))
39+
40+
def _totp_enforce_setup(self, secret, code):
41+
self.ensure_one()
42+
assert self.env.su or (
43+
request
44+
and not request.session.uid
45+
and request.session.get("pre_uid") == self.id
46+
), "Only callable for the user currently in the pre-authentication phase"
47+
secret = compress(secret).upper()
48+
try:
49+
code = int(compress(str(code)))
50+
except ValueError:
51+
return False
52+
if TOTP(base64.b32decode(secret)).match(code) is None:
53+
_logger.info("2FA enforce setup: REJECT CODE for %r", self.login)
54+
return False
55+
self.sudo().totp_secret = secret
56+
_logger.info("2FA enforce setup: SUCCESS for %r", self.login)
57+
return True

auth_totp_enforce/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["whool"]
3+
build-backend = "whool.buildapi"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- ForgeFlow S.L. \<<contact@forgeflow.com>\>
2+
- Laura Cazorla \<<laura.cazorla@forgeflow.com>\>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
This module forces internal users to configure two-factor authentication
2+
(a TOTP authenticator app) before they log in.
3+
4+
Enforcement works like the password expiry flow of `password_security`: nobody
5+
is logged out when the module is installed, but on their next login users are
6+
held in a mandatory step and cannot reach the backend until they have enabled
7+
an authenticator app.
8+
9+
Members of the **Exempt from 2FA enforcement** group are never forced. Portal
10+
and public users are out of scope at the moment.

0 commit comments

Comments
 (0)