Skip to content

Commit a08a0b6

Browse files
committed
[ADD] auth_totp_mail: 2FA using code sent by email
Add the possibility to force the two-factor authentication for all users, using a two-factor authentication by email when the 2FA using an Authenticator app is not configured for the user. Two possibilities: - Force the 2FA only for employee users using the system parameter `auth_totp.policy=employee_required` - Force the 2FA for all users, employees and portals, using the system parameter `auth_totp.policy=all_required` closes odoo#83750 Signed-off-by: Denis Ledoux (dle) <[email protected]>
1 parent 8b5f613 commit a08a0b6

19 files changed

+626
-9
lines changed

.tx/config

+5
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ file_filter = addons/auth_totp_mail/i18n/<lang>.po
9797
source_file = addons/auth_totp_mail/i18n/auth_totp_mail.pot
9898
source_lang = en
9999

100+
[odoo-master.auth_totp_mail_enforce]
101+
file_filter = addons/auth_totp_mail_enforce/i18n/<lang>.po
102+
source_file = addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot
103+
source_lang = en
104+
100105
[odoo-master.auth_totp_portal]
101106
file_filter = addons/auth_totp_portal/i18n/<lang>.po
102107
source_file = addons/auth_totp_portal/i18n/auth_totp_portal.pot

addons/auth_totp/controllers/home.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ def web_totp(self, redirect=None, **kwargs):
3535
request.session.finalize()
3636
return request.redirect(self._login_redirect(request.session.uid, redirect=redirect))
3737

38-
elif user and request.httprequest.method == 'POST':
38+
elif user and request.httprequest.method == 'POST' and kwargs.get('totp_token'):
3939
try:
4040
with user._assert_can_auth():
4141
user._totp_check(int(re.sub(r'\s', '', kwargs['totp_token'])))
42-
except AccessDenied:
43-
error = _("Verification failed, please double-check the 6-digit code")
42+
except AccessDenied as e:
43+
error = str(e)
4444
except ValueError:
4545
error = _("Invalid authentication code format.")
4646
else:
@@ -66,6 +66,7 @@ def web_totp(self, redirect=None, **kwargs):
6666
return response
6767

6868
return request.render('auth_totp.auth_totp_form', {
69+
'user': user,
6970
'error': error,
7071
'redirect': redirect,
7172
})

addons/auth_totp/i18n/auth_totp.pot

+1-1
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ msgid "Verification Code"
372372
msgstr ""
373373

374374
#. module: auth_totp
375-
#: code:addons/auth_totp/controllers/home.py:0
375+
#: code:addons/auth_totp/models/res_users.py:0
376376
#: code:addons/auth_totp/wizard/auth_totp_wizard.py:0
377377
#, python-format
378378
msgid "Verification failed, please double-check the 6-digit code"

addons/auth_totp/models/res_users.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@ class Users(models.Model):
2828
def SELF_READABLE_FIELDS(self):
2929
return super().SELF_READABLE_FIELDS + ['totp_enabled', 'totp_trusted_device_ids']
3030

31+
def _mfa_type(self):
32+
r = super()._mfa_type()
33+
if r is not None:
34+
return r
35+
if self.totp_enabled:
36+
return 'totp'
3137

3238
def _mfa_url(self):
3339
r = super()._mfa_url()
3440
if r is not None:
3541
return r
36-
if self.totp_enabled:
42+
if self._mfa_type() == 'totp':
3743
return '/web/login/totp'
3844

3945
@api.depends('totp_secret')
@@ -55,7 +61,7 @@ def _totp_check(self, code):
5561
match = TOTP(key).match(code)
5662
if match is None:
5763
_logger.info("2FA check: FAIL for %s %r", self, self.login)
58-
raise AccessDenied()
64+
raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
5965
_logger.info("2FA check: SUCCESS for %s %r", self, self.login)
6066

6167
def _totp_try_setting(self, secret, code):

addons/auth_totp/models/totp.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class TOTP:
2020
def __init__(self, key):
2121
self._key = key
2222

23-
def match(self, code, t=None, window=TIMESTEP):
23+
def match(self, code, t=None, window=TIMESTEP, timestep=TIMESTEP):
2424
"""
2525
:param code: authenticator code to check against this key
2626
:param int t: current timestamp (seconds)
@@ -32,8 +32,8 @@ def match(self, code, t=None, window=TIMESTEP):
3232
if t is None:
3333
t = time.time()
3434

35-
low = int((t - window) / TIMESTEP)
36-
high = int((t + window) / TIMESTEP) + 1
35+
low = int((t - window) / timestep)
36+
high = int((t + window) / timestep) + 1
3737

3838
return next((
3939
counter for counter in range(low, high)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import controllers
5+
from . import models
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
'name': '2FA by mail',
3+
'description': """
4+
2FA by mail
5+
===============
6+
Two-Factor authentication by sending a code to the user email inbox
7+
when the 2FA using an authenticator app is not configured.
8+
To enforce users to use a two-factor authentication by default,
9+
and encourage users to configure their 2FA using an authenticator app.
10+
""",
11+
'depends': ['auth_totp', 'mail'],
12+
'category': 'Extra Tools',
13+
'data': [
14+
'data/mail_template_data.xml',
15+
'security/ir.model.access.csv',
16+
'views/res_config_settings_views.xml',
17+
'views/templates.xml',
18+
],
19+
'license': 'LGPL-3',
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- coding: utf-8 -*-
2+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
3+
4+
from . import home
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# -*- coding: utf-8 -*-
2+
import odoo.addons.auth_totp.controllers.home
3+
4+
from odoo import http
5+
from odoo.exceptions import AccessDenied, UserError
6+
from odoo.http import request
7+
8+
9+
class Home(odoo.addons.auth_totp.controllers.home.Home):
10+
@http.route()
11+
def web_totp(self, redirect=None, **kwargs):
12+
response = super().web_totp(redirect=redirect, **kwargs)
13+
if response.status_code != 200 or response.qcontext['user']._mfa_type() != 'totp_mail':
14+
# In case the response from the super is a redirection
15+
# or the user has another TOTP method, we return the response from the call to super.
16+
return response
17+
assert request.session.pre_uid and not request.session.uid, \
18+
"The user must still be in the pre-authentication phase"
19+
20+
# Send the email containing the code to the user inbox
21+
try:
22+
response.qcontext['user']._send_totp_mail_code()
23+
except (AccessDenied, UserError) as e:
24+
response.qcontext['error'] = str(e)
25+
26+
return response
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<data noupdate="1">
4+
<record id="mail_template_totp_mail_code" model="mail.template">
5+
<field name="name">TOTP for users: Authentication by email</field>
6+
<field name="model_id" ref="base.model_res_users" />
7+
<field name="subject">Your two-factor authentication code</field>
8+
<field name="email_to">{{ object.email_formatted }}</field>
9+
<field name="email_from">"{{ object.company_id.name }}" &lt;{{ (object.company_id.email or user.email) }}&gt;</field>
10+
<field name="lang">{{ object.partner_id.lang }}</field>
11+
<field name="auto_delete" eval="True"/>
12+
<field name="body_html" type="html">
13+
<div style="margin: 0px; padding: 0px; font-size: 13px;">
14+
Dear <t t-out="object.partner_id.name or ''"></t><br/><br/>
15+
<p>Someone is trying to log in into your account with a new device.</p>
16+
<ul>
17+
<t t-set="not_available">N/A</t>
18+
<li>Location: <t t-out="ctx.get('location') or not_available"/></li>
19+
<li>Device: <t t-out="ctx.get('device') or not_available"/></li>
20+
<li>Browser: <t t-out="ctx.get('browser') or not_available"/></li>
21+
<li>IP address: <t t-out="ctx.get('ip') or not_available"/></li>
22+
</ul>
23+
<p>If this is you, please enter the following code to complete the login:</p>
24+
<t t-set="code_expiration" t-value="object._get_totp_mail_code()"/>
25+
<t t-set="code" t-value="code_expiration[0]"/>
26+
<t t-set="expiration" t-value="code_expiration[1]"/>
27+
<div style="margin: 16px 0px 16px 0px; text-align: center;">
28+
<span t-out="code" style="background-color:#faf9fa; border: 1px solid #dad8de; padding: 8px 16px 8px 16px; font-size: 24px; color: #875A7B; border-radius: 5px;"/>
29+
</div>
30+
<small>Please note that this code expires in <t t-out="expiration"/>.</small>
31+
32+
<p style="margin: 16px 0px 16px 0px;">
33+
If you did NOT initiate this log-in,
34+
you should immediately change your password to ensure account security.
35+
</p>
36+
37+
<p style="margin: 16px 0px 16px 0px;">
38+
We also strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.
39+
</p>
40+
41+
<p style="margin: 16px 0px 16px 0px; text-align: center;">
42+
<a t-att-href="object.get_totp_invite_url()"
43+
style="background-color:#875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">
44+
Activate my two-factor authentication
45+
</a>
46+
</p>
47+
</div>
48+
</field>
49+
</record>
50+
</data>
51+
</odoo>

0 commit comments

Comments
 (0)