diff --git a/addons/auth_totp/__manifest__.py b/addons/auth_totp/__manifest__.py
index e420254099c..e350d36c9b0 100644
--- a/addons/auth_totp/__manifest__.py
+++ b/addons/auth_totp/__manifest__.py
@@ -18,9 +18,10 @@ can setup API keys to replace their main password.
'category': 'Extra Tools',
'auto_install': True,
'data': [
+ 'security/security.xml',
+ 'security/ir.model.access.csv',
'data/ir_action_data.xml',
'data/mail_template_data.xml',
- 'security/security.xml',
'views/res_users_views.xml',
'views/templates.xml',
'wizard/auth_totp_wizard_views.xml',
diff --git a/addons/auth_totp/controllers/home.py b/addons/auth_totp/controllers/home.py
index 19d678f47a1..1c6f2f879c1 100644
--- a/addons/auth_totp/controllers/home.py
+++ b/addons/auth_totp/controllers/home.py
@@ -6,6 +6,9 @@ from odoo import http, _
from odoo.exceptions import AccessDenied
from odoo.http import request
+TRUSTED_DEVICE_COOKIE = 'td_id'
+TRUSTED_DEVICE_AGE = 90*86400 # 90 days expiration
+
class Home(odoo.addons.web.controllers.main.Home):
@http.route(
@@ -21,8 +24,18 @@ class Home(odoo.addons.web.controllers.main.Home):
return request.redirect('/web/login')
error = None
- if request.httprequest.method == 'POST':
- user = request.env['res.users'].browse(request.session.pre_uid)
+
+ user = request.env['res.users'].browse(request.session.pre_uid)
+ if user and request.httprequest.method == 'GET':
+ cookies = request.httprequest.cookies
+ key = cookies.get(TRUSTED_DEVICE_COOKIE)
+ if key:
+ checked_credentials = request.env['auth_totp.device']._check_credentials(scope="browser", key=key)
+ if checked_credentials == user.id:
+ request.session.finalize()
+ return request.redirect(self._login_redirect(request.session.uid, redirect=redirect))
+
+ elif user and request.httprequest.method == 'POST':
try:
with user._assert_can_auth():
user._totp_check(int(re.sub(r'\s', '', kwargs['totp_token'])))
@@ -32,7 +45,25 @@ class Home(odoo.addons.web.controllers.main.Home):
error = _("Invalid authentication code format.")
else:
request.session.finalize()
- return request.redirect(self._login_redirect(request.session.uid, redirect=redirect))
+ response = request.redirect(self._login_redirect(request.session.uid, redirect=redirect))
+ if kwargs.get('remember'):
+ name = _("%(browser)s on %(platform)s",
+ browser=request.httprequest.user_agent.browser.capitalize(),
+ platform=request.httprequest.user_agent.platform.capitalize(),
+ )
+ geoip = request.session.geoip
+ if geoip:
+ name += " (%s, %s)" % (geoip['city'], geoip['country_name'])
+
+ key = request.env['auth_totp.device']._generate("browser", name)
+ response.set_cookie(
+ key=TRUSTED_DEVICE_COOKIE,
+ value=key,
+ max_age=TRUSTED_DEVICE_AGE,
+ httponly=True,
+ samesite='Lax'
+ )
+ return response
return request.render('auth_totp.auth_totp_form', {
'error': error,
diff --git a/addons/auth_totp/models/__init__.py b/addons/auth_totp/models/__init__.py
index 563dd656946..94805ad2cef 100644
--- a/addons/auth_totp/models/__init__.py
+++ b/addons/auth_totp/models/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import auth_totp
from . import ir_http
from . import res_users
from . import totp
diff --git a/addons/auth_totp/models/auth_totp.py b/addons/auth_totp/models/auth_totp.py
new file mode 100644
index 00000000000..38ac76df4a4
--- /dev/null
+++ b/addons/auth_totp/models/auth_totp.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+from odoo import models
+
+
+class AuthTotpDevice(models.Model):
+
+ # init is overriden in res.users.apikeys to create a secret column 'key'
+ # use a different model to benefit from the secured methods while not mixing
+ # two different concepts
+
+ _name = "auth_totp.device"
+ _inherit = "res.users.apikeys"
+ _description = "Authentication Device"
+ _auto = False
diff --git a/addons/auth_totp/models/res_users.py b/addons/auth_totp/models/res_users.py
index 708c9bd87c5..8e19092de32 100644
--- a/addons/auth_totp/models/res_users.py
+++ b/addons/auth_totp/models/res_users.py
@@ -22,10 +22,12 @@ class Users(models.Model):
totp_secret = fields.Char(copy=False, groups=fields.NO_ACCESS)
totp_enabled = fields.Boolean(string="Two-factor authentication", compute='_compute_totp_enabled')
+ totp_trusted_device_ids = fields.One2many('auth_totp.device', 'user_id', string="Trusted Devices")
@property
def SELF_READABLE_FIELDS(self):
- return super().SELF_READABLE_FIELDS + ['totp_enabled']
+ return super().SELF_READABLE_FIELDS + ['totp_enabled', 'totp_trusted_device_ids']
+
def _mfa_url(self):
r = super()._mfa_url()
@@ -120,7 +122,9 @@ class Users(models.Model):
_logger.info("2FA disable: REJECT for %s (%s) by uid #%s", self, logins, self.env.user.id)
return False
+ self.revoke_all_devices()
self.sudo().write({'totp_secret': False})
+
if request and self == self.env.user:
self.flush()
# update session token so the user does not get logged out (cache cleared by change)
@@ -163,3 +167,14 @@ class Users(models.Model):
'views': [(False, 'form')],
'context': self.env.context,
}
+
+ @check_identity
+ def revoke_all_devices(self):
+ self._revoke_all_devices()
+
+ def _revoke_all_devices(self):
+ self.totp_trusted_device_ids._remove()
+
+ def change_password(self, old_passwd, new_passwd):
+ self.env.user._revoke_all_devices()
+ return super().change_password(old_passwd, new_passwd)
diff --git a/addons/auth_totp/security/ir.model.access.csv b/addons/auth_totp/security/ir.model.access.csv
new file mode 100644
index 00000000000..4a116ba291c
--- /dev/null
+++ b/addons/auth_totp/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
+"access_auth_totp_device_access_employee","TOTP Device access employees","model_auth_totp_device","base.group_user",1,0,0,0
+"access_auth_totp_device_access_portal","TOTP Device access portal","model_auth_totp_device","base.group_portal",1,0,0,0
diff --git a/addons/auth_totp/security/security.xml b/addons/auth_totp/security/security.xml
index 4d44c23a457..5d6a455508f 100644
--- a/addons/auth_totp/security/security.xml
+++ b/addons/auth_totp/security/security.xml
@@ -13,4 +13,27 @@