[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/odoo#83750 Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
This commit is contained in:
@@ -97,6 +97,11 @@ file_filter = addons/auth_totp_mail/i18n/<lang>.po
|
||||
source_file = addons/auth_totp_mail/i18n/auth_totp_mail.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.auth_totp_mail_enforce]
|
||||
file_filter = addons/auth_totp_mail_enforce/i18n/<lang>.po
|
||||
source_file = addons/auth_totp_mail_enforce/i18n/auth_totp_mail_enforce.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.auth_totp_portal]
|
||||
file_filter = addons/auth_totp_portal/i18n/<lang>.po
|
||||
source_file = addons/auth_totp_portal/i18n/auth_totp_portal.pot
|
||||
|
||||
@@ -35,12 +35,12 @@ class Home(odoo.addons.web.controllers.main.Home):
|
||||
request.session.finalize()
|
||||
return request.redirect(self._login_redirect(request.session.uid, redirect=redirect))
|
||||
|
||||
elif user and request.httprequest.method == 'POST':
|
||||
elif user and request.httprequest.method == 'POST' and kwargs.get('totp_token'):
|
||||
try:
|
||||
with user._assert_can_auth():
|
||||
user._totp_check(int(re.sub(r'\s', '', kwargs['totp_token'])))
|
||||
except AccessDenied:
|
||||
error = _("Verification failed, please double-check the 6-digit code")
|
||||
except AccessDenied as e:
|
||||
error = str(e)
|
||||
except ValueError:
|
||||
error = _("Invalid authentication code format.")
|
||||
else:
|
||||
@@ -66,6 +66,7 @@ class Home(odoo.addons.web.controllers.main.Home):
|
||||
return response
|
||||
|
||||
return request.render('auth_totp.auth_totp_form', {
|
||||
'user': user,
|
||||
'error': error,
|
||||
'redirect': redirect,
|
||||
})
|
||||
|
||||
@@ -372,7 +372,7 @@ msgid "Verification Code"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp
|
||||
#: code:addons/auth_totp/controllers/home.py:0
|
||||
#: code:addons/auth_totp/models/res_users.py:0
|
||||
#: code:addons/auth_totp/wizard/auth_totp_wizard.py:0
|
||||
#, python-format
|
||||
msgid "Verification failed, please double-check the 6-digit code"
|
||||
|
||||
@@ -28,12 +28,18 @@ class Users(models.Model):
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + ['totp_enabled', 'totp_trusted_device_ids']
|
||||
|
||||
def _mfa_type(self):
|
||||
r = super()._mfa_type()
|
||||
if r is not None:
|
||||
return r
|
||||
if self.totp_enabled:
|
||||
return 'totp'
|
||||
|
||||
def _mfa_url(self):
|
||||
r = super()._mfa_url()
|
||||
if r is not None:
|
||||
return r
|
||||
if self.totp_enabled:
|
||||
if self._mfa_type() == 'totp':
|
||||
return '/web/login/totp'
|
||||
|
||||
@api.depends('totp_secret')
|
||||
@@ -55,7 +61,7 @@ class Users(models.Model):
|
||||
match = TOTP(key).match(code)
|
||||
if match is None:
|
||||
_logger.info("2FA check: FAIL for %s %r", self, self.login)
|
||||
raise AccessDenied()
|
||||
raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
|
||||
_logger.info("2FA check: SUCCESS for %s %r", self, self.login)
|
||||
|
||||
def _totp_try_setting(self, secret, code):
|
||||
|
||||
@@ -20,7 +20,7 @@ class TOTP:
|
||||
def __init__(self, key):
|
||||
self._key = key
|
||||
|
||||
def match(self, code, t=None, window=TIMESTEP):
|
||||
def match(self, code, t=None, window=TIMESTEP, timestep=TIMESTEP):
|
||||
"""
|
||||
:param code: authenticator code to check against this key
|
||||
:param int t: current timestamp (seconds)
|
||||
@@ -32,8 +32,8 @@ class TOTP:
|
||||
if t is None:
|
||||
t = time.time()
|
||||
|
||||
low = int((t - window) / TIMESTEP)
|
||||
high = int((t + window) / TIMESTEP) + 1
|
||||
low = int((t - window) / timestep)
|
||||
high = int((t + window) / timestep) + 1
|
||||
|
||||
return next((
|
||||
counter for counter in range(low, high)
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
'name': '2FA by mail',
|
||||
'description': """
|
||||
2FA by mail
|
||||
===============
|
||||
Two-Factor authentication by sending a code to the user email inbox
|
||||
when the 2FA using an authenticator app is not configured.
|
||||
To enforce users to use a two-factor authentication by default,
|
||||
and encourage users to configure their 2FA using an authenticator app.
|
||||
""",
|
||||
'depends': ['auth_totp', 'mail'],
|
||||
'category': 'Extra Tools',
|
||||
'data': [
|
||||
'data/mail_template_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/templates.xml',
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import home
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import odoo.addons.auth_totp.controllers.home
|
||||
|
||||
from odoo import http
|
||||
from odoo.exceptions import AccessDenied, UserError
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class Home(odoo.addons.auth_totp.controllers.home.Home):
|
||||
@http.route()
|
||||
def web_totp(self, redirect=None, **kwargs):
|
||||
response = super().web_totp(redirect=redirect, **kwargs)
|
||||
if response.status_code != 200 or response.qcontext['user']._mfa_type() != 'totp_mail':
|
||||
# In case the response from the super is a redirection
|
||||
# or the user has another TOTP method, we return the response from the call to super.
|
||||
return response
|
||||
assert request.session.pre_uid and not request.session.uid, \
|
||||
"The user must still be in the pre-authentication phase"
|
||||
|
||||
# Send the email containing the code to the user inbox
|
||||
try:
|
||||
response.qcontext['user']._send_totp_mail_code()
|
||||
except (AccessDenied, UserError) as e:
|
||||
response.qcontext['error'] = str(e)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="mail_template_totp_mail_code" model="mail.template">
|
||||
<field name="name">TOTP for users: Authentication by email</field>
|
||||
<field name="model_id" ref="base.model_res_users" />
|
||||
<field name="subject">Your two-factor authentication code</field>
|
||||
<field name="email_to">{{ object.email_formatted }}</field>
|
||||
<field name="email_from">"{{ object.company_id.name }}" <{{ (object.company_id.email or user.email) }}></field>
|
||||
<field name="lang">{{ object.partner_id.lang }}</field>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<field name="body_html" type="html">
|
||||
<div style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||
Dear <t t-out="object.partner_id.name or ''"></t><br/><br/>
|
||||
<p>Someone is trying to log in into your account with a new device.</p>
|
||||
<ul>
|
||||
<t t-set="not_available">N/A</t>
|
||||
<li>Location: <t t-out="ctx.get('location') or not_available"/></li>
|
||||
<li>Device: <t t-out="ctx.get('device') or not_available"/></li>
|
||||
<li>Browser: <t t-out="ctx.get('browser') or not_available"/></li>
|
||||
<li>IP address: <t t-out="ctx.get('ip') or not_available"/></li>
|
||||
</ul>
|
||||
<p>If this is you, please enter the following code to complete the login:</p>
|
||||
<t t-set="code_expiration" t-value="object._get_totp_mail_code()"/>
|
||||
<t t-set="code" t-value="code_expiration[0]"/>
|
||||
<t t-set="expiration" t-value="code_expiration[1]"/>
|
||||
<div style="margin: 16px 0px 16px 0px; text-align: center;">
|
||||
<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;"/>
|
||||
</div>
|
||||
<small>Please note that this code expires in <t t-out="expiration"/>.</small>
|
||||
|
||||
<p style="margin: 16px 0px 16px 0px;">
|
||||
If you did NOT initiate this log-in,
|
||||
you should immediately change your password to ensure account security.
|
||||
</p>
|
||||
|
||||
<p style="margin: 16px 0px 16px 0px;">
|
||||
We also strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.
|
||||
</p>
|
||||
|
||||
<p style="margin: 16px 0px 16px 0px; text-align: center;">
|
||||
<a t-att-href="object.get_totp_invite_url()"
|
||||
style="background-color:#875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;">
|
||||
Activate my two-factor authentication
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,229 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * auth_totp_mail_enforce
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 15.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2022-02-01 12:13+0000\n"
|
||||
"PO-Revision-Date: 2022-02-01 12:13+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form
|
||||
msgid ""
|
||||
".\n"
|
||||
" <br/>"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:mail.template,body_html:auth_totp_mail_enforce.mail_template_totp_mail_code
|
||||
msgid ""
|
||||
"<div style=\"margin: 0px; padding: 0px; font-size: 13px;\">\n"
|
||||
" Dear <t t-out=\"object.partner_id.name or ''\"/><br/><br/>\n"
|
||||
" <p>Someone is trying to log in into your account with a new device.</p>\n"
|
||||
" <ul>\n"
|
||||
" <t t-set=\"not_available\">N/A</t>\n"
|
||||
" <li>Location: <t t-out=\"ctx.get('location') or not_available\"/></li>\n"
|
||||
" <li>Device: <t t-out=\"ctx.get('device') or not_available\"/></li>\n"
|
||||
" <li>Browser: <t t-out=\"ctx.get('browser') or not_available\"/></li>\n"
|
||||
" <li>IP address: <t t-out=\"ctx.get('ip') or not_available\"/></li>\n"
|
||||
" </ul>\n"
|
||||
" <p>If this is you, please enter the following code to complete the login:</p>\n"
|
||||
" <t t-set=\"code_expiration\" t-value=\"object._get_totp_mail_code()\"/>\n"
|
||||
" <t t-set=\"code\" t-value=\"code_expiration[0]\"/>\n"
|
||||
" <t t-set=\"expiration\" t-value=\"code_expiration[1]\"/>\n"
|
||||
" <div style=\"margin: 16px 0px 16px 0px; text-align: center;\">\n"
|
||||
" <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;\"/>\n"
|
||||
" </div>\n"
|
||||
" <small>Please note that this code expires in <t t-out=\"expiration\"/>.</small>\n"
|
||||
"\n"
|
||||
" <p style=\"margin: 16px 0px 16px 0px;\">\n"
|
||||
" If you did NOT initiate this log-in,\n"
|
||||
" you should immediately change your password to ensure account security.\n"
|
||||
" </p>\n"
|
||||
"\n"
|
||||
" <p style=\"margin: 16px 0px 16px 0px;\">\n"
|
||||
" We also strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.\n"
|
||||
" </p>\n"
|
||||
"\n"
|
||||
" <p style=\"margin: 16px 0px 16px 0px; text-align: center;\">\n"
|
||||
" <a t-att-href=\"object.get_totp_invite_url()\" style=\"background-color:#875A7B; padding: 8px 16px 8px 16px; text-decoration: none; color: #fff; border-radius: 5px;\">\n"
|
||||
" Activate my two-factor authentication\n"
|
||||
" </a>\n"
|
||||
" </p>\n"
|
||||
"</div>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form
|
||||
msgid ""
|
||||
"<i class=\"fa fa-envelope-o\"/>\n"
|
||||
" To login, enter below the six-digit authentication code just sent via email to"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__res_config_settings__auth_totp_policy__all_required
|
||||
msgid "All users"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: code:addons/auth_totp_mail_enforce/models/res_users.py:0
|
||||
#, python-format
|
||||
msgid "Cannot send email: user %s has no email address."
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__auth_totp_rate_limit_log__limit_type__code_check
|
||||
msgid "Code Checking"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model,name:auth_totp_mail_enforce.model_res_config_settings
|
||||
msgid "Config Settings"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__res_config_settings__auth_totp_policy__employee_required
|
||||
msgid "Employees only"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.res_config_settings_view_form
|
||||
msgid ""
|
||||
"Enforce the two-factor authentication by email for employees or for all users\n"
|
||||
" (including portal users) if they didn't enable any other two-factor authentication\n"
|
||||
" method."
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_res_config_settings__auth_totp_enforce
|
||||
msgid "Enforce two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__ip
|
||||
msgid "Ip"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form
|
||||
msgid "Learn More"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__limit_type
|
||||
msgid "Limit Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form
|
||||
msgid "Re-send email"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields.selection,name:auth_totp_mail_enforce.selection__auth_totp_rate_limit_log__limit_type__send_email
|
||||
msgid "Send Email"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__source
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:mail.template,name:auth_totp_mail_enforce.mail_template_totp_mail_code
|
||||
msgid "TOTP for users: Authentication by email"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model,name:auth_totp_mail_enforce.model_auth_totp_rate_limit_log
|
||||
msgid "TOTP rate limit logs"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_res_config_settings__auth_totp_policy
|
||||
msgid "Two-factor authentication enforcing policy"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model.fields,field_description:auth_totp_mail_enforce.field_auth_totp_rate_limit_log__user_id
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:ir.model,name:auth_totp_mail_enforce.model_res_users
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: code:addons/auth_totp_mail_enforce/models/res_users.py:0
|
||||
#, python-format
|
||||
msgid "Verification failed, please double-check the 6-digit code"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model_terms:ir.ui.view,arch_db:auth_totp_mail_enforce.auth_totp_mail_form
|
||||
msgid ""
|
||||
"We strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.\n"
|
||||
" <br/>"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: code:addons/auth_totp_mail_enforce/models/res_users.py:0
|
||||
#, python-format
|
||||
msgid "You reached the limit of authentication mails sent for your account"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: code:addons/auth_totp_mail_enforce/models/res_users.py:0
|
||||
#, python-format
|
||||
msgid "You reached the limit of code verifications for your account"
|
||||
msgstr ""
|
||||
|
||||
#. module: auth_totp_mail_enforce
|
||||
#: model:mail.template,subject:auth_totp_mail_enforce.mail_template_totp_mail_code
|
||||
msgid "Your two-factor authentication code"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import auth_totp_rate_limit_log
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
@@ -0,0 +1,20 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class AuthTotpRateLimitLog(models.TransientModel):
|
||||
_name = 'auth.totp.rate.limit.log'
|
||||
_description = 'TOTP rate limit logs'
|
||||
|
||||
def init(self):
|
||||
self.env.cr.execute("""
|
||||
CREATE INDEX IF NOT EXISTS auth_totp_rate_limit_log_user_id_limit_type_create_date_idx
|
||||
ON auth_totp_rate_limit_log(user_id, limit_type, create_date);
|
||||
""")
|
||||
|
||||
user_id = fields.Many2one('res.users', required=True, readonly=True)
|
||||
scope = fields.Char(readonly=True)
|
||||
ip = fields.Char(readonly=True)
|
||||
limit_type = fields.Selection([
|
||||
('send_email', 'Send Email'),
|
||||
('code_check', 'Code Checking'),
|
||||
], readonly=True)
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
auth_totp_enforce = fields.Boolean(
|
||||
string="Enforce two-factor authentication",
|
||||
)
|
||||
auth_totp_policy = fields.Selection([
|
||||
('employee_required', 'Employees only'),
|
||||
('all_required', 'All users')
|
||||
],
|
||||
string="Two-factor authentication enforcing policy",
|
||||
config_parameter='auth_totp.policy',
|
||||
)
|
||||
|
||||
@api.onchange('auth_totp_enforce')
|
||||
def _onchange_auth_totp_enforce(self):
|
||||
if self.auth_totp_enforce:
|
||||
self.auth_totp_policy = 'employee_required'
|
||||
else:
|
||||
self.auth_totp_policy = False
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
res['auth_totp_enforce'] = bool(self.env['ir.config_parameter'].sudo().get_param('auth_totp.policy'))
|
||||
return res
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import babel.dates
|
||||
import logging
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from odoo import _, models
|
||||
from odoo.exceptions import AccessDenied, UserError
|
||||
from odoo.http import request
|
||||
from odoo.tools.misc import babel_locale_parse, hmac
|
||||
|
||||
from odoo.addons.auth_totp.models.totp import hotp, TOTP
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
TOTP_RATE_LIMITS = {
|
||||
'send_email': (10, 3600),
|
||||
'code_check': (10, 3600),
|
||||
}
|
||||
|
||||
|
||||
class Users(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
def _mfa_type(self):
|
||||
r = super()._mfa_type()
|
||||
if r is not None:
|
||||
return r
|
||||
ICP = self.env['ir.config_parameter'].sudo()
|
||||
otp_required = False
|
||||
if ICP.get_param('auth_totp.policy') == 'all_required':
|
||||
otp_required = True
|
||||
elif ICP.get_param('auth_totp.policy') == 'employee_required' and self.has_group('base.group_user'):
|
||||
otp_required = True
|
||||
if otp_required:
|
||||
return 'totp_mail'
|
||||
|
||||
def _mfa_url(self):
|
||||
r = super()._mfa_url()
|
||||
if r is not None:
|
||||
return r
|
||||
if self._mfa_type() == 'totp_mail':
|
||||
return '/web/login/totp'
|
||||
|
||||
def _totp_check(self, code):
|
||||
self._totp_rate_limit('code_check')
|
||||
user = self.sudo()
|
||||
if user._mfa_type() != 'totp_mail':
|
||||
return super()._totp_check(code)
|
||||
|
||||
key = self._get_totp_mail_key()
|
||||
match = TOTP(key).match(code, window=3600, timestep=3600)
|
||||
if match is None:
|
||||
_logger.info("2FA check (mail): FAIL for %s %r", self, self.login)
|
||||
raise AccessDenied(_("Verification failed, please double-check the 6-digit code"))
|
||||
_logger.info("2FA check(mail): SUCCESS for %s %r", self, self.login)
|
||||
self._totp_rate_limit_purge('code_check')
|
||||
self._totp_rate_limit_purge('send_email')
|
||||
return True
|
||||
|
||||
def _get_totp_mail_key(self):
|
||||
self.ensure_one()
|
||||
return hmac(self.env(su=True), 'auth_totp_mail-code', (self.id, self.login, self.login_date)).encode()
|
||||
|
||||
def _get_totp_mail_code(self):
|
||||
self.ensure_one()
|
||||
|
||||
key = self._get_totp_mail_key()
|
||||
|
||||
now = datetime.now()
|
||||
counter = int(datetime.timestamp(now) / 3600)
|
||||
|
||||
code = hotp(key, counter)
|
||||
expiration = timedelta(seconds=3600)
|
||||
lang = babel_locale_parse(self.env.context.get('lang') or self.lang)
|
||||
expiration = babel.dates.format_timedelta(expiration, lang)
|
||||
|
||||
return str(code).zfill(6), expiration
|
||||
|
||||
def _send_totp_mail_code(self):
|
||||
self.ensure_one()
|
||||
self._totp_rate_limit('send_email')
|
||||
|
||||
if not self.email:
|
||||
raise UserError(_("Cannot send email: user %s has no email address.", self.name))
|
||||
|
||||
template = self.env.ref('auth_totp_mail_enforce.mail_template_totp_mail_code').sudo()
|
||||
context = {}
|
||||
if request:
|
||||
geoip = request.session.geoip
|
||||
context.update({
|
||||
'location': f"{geoip['city']}, {geoip['country_name']}" if geoip else None,
|
||||
'device': request.httprequest.user_agent.platform.capitalize(),
|
||||
'browser': request.httprequest.user_agent.browser.capitalize(),
|
||||
'ip': request.httprequest.environ['REMOTE_ADDR'],
|
||||
})
|
||||
email_values = {
|
||||
'email_to': self.email,
|
||||
'email_cc': False,
|
||||
'auto_delete': True,
|
||||
'recipient_ids': [],
|
||||
'partner_ids': [],
|
||||
'scheduled_date': False,
|
||||
}
|
||||
with self.env.cr.savepoint():
|
||||
template.with_context(**context).send_mail(
|
||||
self.id, force_send=True, raise_exception=True, email_values=email_values, email_layout_xmlid='mail.mail_notification_light'
|
||||
)
|
||||
|
||||
def _totp_rate_limit(self, limit_type):
|
||||
self.ensure_one()
|
||||
assert request, "A request is required to be able to rate limit TOTP related actions"
|
||||
limit, interval = TOTP_RATE_LIMITS.get(limit_type)
|
||||
RateLimitLog = self.env['auth.totp.rate.limit.log'].sudo()
|
||||
ip = request.httprequest.environ['REMOTE_ADDR']
|
||||
domain = [
|
||||
('user_id', '=', self.id),
|
||||
('create_date', '>=', datetime.now() - timedelta(seconds=interval)),
|
||||
('limit_type', '=', limit_type),
|
||||
('ip', '=', ip),
|
||||
]
|
||||
count = RateLimitLog.search_count(domain)
|
||||
if count >= limit:
|
||||
descriptions = {
|
||||
'send_email': _('You reached the limit of authentication mails sent for your account'),
|
||||
'code_check': _('You reached the limit of code verifications for your account'),
|
||||
}
|
||||
description = descriptions.get(limit_type)
|
||||
raise AccessDenied(description)
|
||||
RateLimitLog.create({
|
||||
'user_id': self.id,
|
||||
'ip': ip,
|
||||
'limit_type': limit_type,
|
||||
})
|
||||
|
||||
def _totp_rate_limit_purge(self, limit_type):
|
||||
self.ensure_one()
|
||||
assert request, "A request is required to be able to rate limit TOTP related actions"
|
||||
ip = request.httprequest.environ['REMOTE_ADDR']
|
||||
RateLimitLog = self.env['auth.totp.rate.limit.log'].sudo()
|
||||
RateLimitLog.search([
|
||||
('user_id', '=', self.id),
|
||||
('limit_type', '=', limit_type),
|
||||
('ip', '=', ip),
|
||||
]).unlink()
|
||||
@@ -0,0 +1,2 @@
|
||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_auth_totp_rate_limit_log","access_auth_totp_rate_limit_log","model_auth_totp_rate_limit_log","base.group_user",0,0,0,0
|
||||
|
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.auth_totp_mail_enforce</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="40"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='allow_import']" position="before">
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="auth_totp_policy">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="auth_totp_enforce" />
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="auth_totp_policy"/>
|
||||
<div class="text-muted">
|
||||
Enforce the two-factor authentication by email for employees or for all users
|
||||
(including portal users) if they didn't enable any other two-factor authentication
|
||||
method.
|
||||
</div>
|
||||
<div class="mt16" attrs="{'invisible': [('auth_totp_enforce', '=', False)]}">
|
||||
<field name="auth_totp_policy" class="o_light_label" widget="radio"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,28 @@
|
||||
<odoo>
|
||||
<template id="auth_totp_mail_form" inherit_id="auth_totp.auth_totp_form">
|
||||
<xpath expr="//form/div[1]" position="attributes">
|
||||
<attribute name="t-if">user._mfa_type() == 'totp'</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//form/div[1]" position="after">
|
||||
<div t-if="user._mfa_type() == 'totp_mail'" class="mb-2 mt-2 text-muted">
|
||||
<i class="fa fa-envelope-o"/>
|
||||
To login, enter below the six-digit authentication code just sent via email to <t t-out="user.email"/>.
|
||||
<br/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//form[1]" position="after">
|
||||
<form method="POST" t-if="user._mfa_type() == 'totp_mail'">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" name="send_email" value="1"/>
|
||||
<button type="submit" class="btn btn-secondary btn-block">Re-send email</button>
|
||||
</form>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('border-top')]" position="before">
|
||||
<div class="mb-2" t-if="user._mfa_type() == 'totp_mail'">
|
||||
We strongly recommend enabling the two-factor authentication using an authenticator app to help secure your account.
|
||||
<br/>
|
||||
<a href="https://www.odoo.com/documentation/15.0/applications/general/auth/2fa.html" title="Learn More" target="_blank">Learn More</a>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -1034,6 +1034,9 @@ class Users(models.Model):
|
||||
if hasattr(self, 'check_credentials'):
|
||||
_logger.warning("The check_credentials method of res.users has been renamed _check_credentials. One of your installed modules defines one, but it will not be called anymore.")
|
||||
|
||||
def _mfa_type(self):
|
||||
""" If an MFA method is enabled, returns its type as a string. """
|
||||
return
|
||||
|
||||
def _mfa_url(self):
|
||||
""" If an MFA method is enabled, returns the URL for its second step. """
|
||||
|
||||
Reference in New Issue
Block a user