diff --git a/addons/auth_totp/__init__.py b/addons/auth_totp/__init__.py
index 9e5827f90ee..cec04a5b0e2 100644
--- a/addons/auth_totp/__init__.py
+++ b/addons/auth_totp/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
+from . import wizard
diff --git a/addons/auth_totp/__manifest__.py b/addons/auth_totp/__manifest__.py
index 7005abfb8b6..05ad46b63c3 100644
--- a/addons/auth_totp/__manifest__.py
+++ b/addons/auth_totp/__manifest__.py
@@ -18,10 +18,12 @@ can setup API keys to replace their main password.
'category': 'Extra Tools',
'auto_install': True,
'data': [
+ 'data/ir_action_data.xml',
'data/mail_templates_data.xml',
'security/security.xml',
- 'views/user_preferences.xml',
+ 'views/res_users_views.xml',
'views/templates.xml',
+ 'wizard/auth_totp_wizard_views.xml',
],
'assets': {
'web.assets_tests': [
diff --git a/addons/auth_totp/data/ir_action_data.xml b/addons/auth_totp/data/ir_action_data.xml
new file mode 100644
index 00000000000..9543556ed08
--- /dev/null
+++ b/addons/auth_totp/data/ir_action_data.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ Invite to use two-factor authentication
+
+
+ list
+ code
+
+ action = records.action_totp_invite()
+
+
+
+
+
+
+ Disable two-factor authentication
+
+
+ list
+ code
+
+ action = records.action_totp_disable()
+
+
+
+
+
+
+ Open two-factor authentication configuration
+
+ code
+
+user = env.user
+action = user.action_open_my_account_settings()
+
+
+
+
diff --git a/addons/auth_totp/data/mail_templates_data.xml b/addons/auth_totp/data/mail_templates_data.xml
index a17c9b105e1..1e31d764915 100644
--- a/addons/auth_totp/data/mail_templates_data.xml
+++ b/addons/auth_totp/data/mail_templates_data.xml
@@ -13,7 +13,9 @@
Dear ${object.partner_id.name}
${user.name} requested you activate two-factor authentication to protect your account.
- Two-factor Authentication ("2FA") is a system of double authentication. The first one is done with your password and the second one with a code you get from a dedicated mobile app. Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
+ Two-factor Authentication ("2FA") is a system of double authentication.
+ The first one is done with your password and the second one with a code you get from a dedicated mobile app.
+ Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
Q", counter)
- mac = hmac.new(secret, msg=C, digestmod=ALGORITHM).digest()
- # the data offset is the last nibble of the hash
- offset = mac[-1] & 0xF
- # code is the 4 bytes at the offset interpreted as a 31b big-endian uint
- # (31b to avoid sign concerns). This effectively limits digits to 9 and
- # hard-limits it to 10: each digit is normally worth 3.32 bits but the
- # 10th is only worth 1.1 (9 digits encode 29.9 bits).
- code = struct.unpack_from('>I', mac, offset)[0] & 0x7FFFFFFF
- r = code % (10 ** DIGITS)
- # NOTE: use text / bytes instead of int?
- return r
diff --git a/addons/auth_totp/models/totp.py b/addons/auth_totp/models/totp.py
new file mode 100644
index 00000000000..642fb8fcc81
--- /dev/null
+++ b/addons/auth_totp/models/totp.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import hmac
+import struct
+import time
+
+# 160 bits, as recommended by HOTP RFC 4226, section 4, R6.
+# Google Auth uses 80 bits by default but supports 160.
+TOTP_SECRET_SIZE = 160
+
+# The algorithm (and key URI format) allows customising these parameters but
+# google authenticator doesn't support it
+# https://github.com/google/google-authenticator/wiki/Key-Uri-Format
+ALGORITHM = 'sha1'
+DIGITS = 6
+TIMESTEP = 30
+
+class TOTP:
+ def __init__(self, key):
+ self._key = key
+
+ def match(self, code, t=None, window=TIMESTEP):
+ """
+ :param code: authenticator code to check against this key
+ :param int t: current timestamp (seconds)
+ :param int window: fuzz window to account for slow fingers, network
+ latency, desynchronised clocks, ..., every code
+ valid between t-window an t+window is considered
+ valid
+ """
+ if t is None:
+ t = time.time()
+
+ low = int((t - window) / TIMESTEP)
+ high = int((t + window) / TIMESTEP) + 1
+
+ return next((
+ counter for counter in range(low, high)
+ if hotp(self._key, counter) == code
+ ), None)
+
+def hotp(secret, counter):
+ # C is the 64b counter encoded in big-endian
+ C = struct.pack(">Q", counter)
+ mac = hmac.new(secret, msg=C, digestmod=ALGORITHM).digest()
+ # the data offset is the last nibble of the hash
+ offset = mac[-1] & 0xF
+ # code is the 4 bytes at the offset interpreted as a 31b big-endian uint
+ # (31b to avoid sign concerns). This effectively limits digits to 9 and
+ # hard-limits it to 10: each digit is normally worth 3.32 bits but the
+ # 10th is only worth 1.1 (9 digits encode 29.9 bits).
+ code = struct.unpack_from('>I', mac, offset)[0] & 0x7FFFFFFF
+ r = code % (10 ** DIGITS)
+ # NOTE: use text / bytes instead of int?
+ return r
diff --git a/addons/auth_totp/views/res_users_views.xml b/addons/auth_totp/views/res_users_views.xml
new file mode 100644
index 00000000000..43d116ae405
--- /dev/null
+++ b/addons/auth_totp/views/res_users_views.xml
@@ -0,0 +1,115 @@
+
+
+
+ res.users.view.search.inherit.auth.totp
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
+ user form: add totp status
+ res.users
+
+
+
+
+
+
+
+
+
Two-factor Authentication
+
+
+
+
+
+
+
+
+ Two-factor Authentication ("2FA") is a system of double authentication.
+ The first one is done with your password and the second one with a code you get from a dedicated mobile app.
+ Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
+ Learn More
+
+
Your account is protected!
+
+
+
+
+
+
+
+
+ users preference: totp
+ res.users
+
+
+
+
+
+
+
Two-factor Authentication
+
+
+
+
+ Two-factor Authentication ("2FA") is a system of double authentication.
+ The first one is done with your password and the second one with a code you get from a dedicated mobile app.
+ Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
+ Learn More
+
+
Your account is protected!
+
+
+
+
+
+
+
+
+ users preference: Account Security
+ res.users
+
+ primary
+
+
+
+
+
+
+ $0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/auth_totp/views/user_preferences.xml b/addons/auth_totp/views/user_preferences.xml
deleted file mode 100644
index efb5ef426e6..00000000000
--- a/addons/auth_totp/views/user_preferences.xml
+++ /dev/null
@@ -1,220 +0,0 @@
-
-
- res.users.view.search.inherit.auth.totp
- res.users
-
-
-
-
-
-
-
-
-
-
-
-
- user form: add totp status
- res.users
-
-
-
-
-
-
-
-
-
Two-factor Authentication
-
-
-
-
-
-
-
-
- Two-factor Authentication ("2FA") is a system of double authentication. The first one is done with your password and the second one with a code you get from a dedicated mobile app. Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
- Learn More
-
-
Your account is protected!
-
-
-
-
-
-
-
-
- Disable two-factor authentication
-
-
- list
- code
-
- action = records.action_totp_disable()
-
-
-
-
-
- auth_totp wizard
- auth_totp.wizard
-
-
-
-
-
-
- users preference: totp
- res.users
-
-
-
-
-
-
-
Two-factor Authentication
-
-
-
-
- Two-factor Authentication ("2FA") is a system of double authentication. The first one is done with your password and the second one with a code you get from a dedicated mobile app. Popular ones include Authy, Google Authenticator or the Microsoft Authenticator.
- Learn More
-
-
Your account is protected!
-
-
-
-
-
-
-
-
- Invite to use two-factor authentication
-
-
- list
- code
-
- action = records.action_totp_invite()
-
-
-
-
-
- users preference: Account Security
- res.users
-
- primary
-
-
-
-
-
-
- $0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Open two-factor authentication configuration
-
- code
-
-user = env.user
-action = user.action_open_my_account_settings()
-
-
-
-
-
diff --git a/addons/auth_totp/wizard/__init__.py b/addons/auth_totp/wizard/__init__.py
new file mode 100644
index 00000000000..ab00fe3257f
--- /dev/null
+++ b/addons/auth_totp/wizard/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import auth_totp_wizard
diff --git a/addons/auth_totp/wizard/auth_totp_wizard.py b/addons/auth_totp/wizard/auth_totp_wizard.py
new file mode 100644
index 00000000000..a8be71df27b
--- /dev/null
+++ b/addons/auth_totp/wizard/auth_totp_wizard.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import base64
+import functools
+import io
+import qrcode
+import re
+import werkzeug.urls
+
+from odoo import _, api, fields, models
+from odoo.addons.base.models.res_users import check_identity
+from odoo.exceptions import UserError
+from odoo.http import request
+
+from odoo.addons.auth_totp.models.totp import ALGORITHM, DIGITS, TIMESTEP
+
+compress = functools.partial(re.sub, r'\s', '')
+
+class TOTPWizard(models.TransientModel):
+ _name = 'auth_totp.wizard'
+ _description = "2-Factor Setup Wizard"
+
+ user_id = fields.Many2one('res.users', required=True, readonly=True)
+ secret = fields.Char(required=True, readonly=True)
+ url = fields.Char(store=True, readonly=True, compute='_compute_qrcode')
+ qrcode = fields.Binary(
+ attachment=False, store=True, readonly=True,
+ compute='_compute_qrcode',
+ )
+ code = fields.Char(string="Verification Code", size=7)
+
+ @api.depends('user_id.login', 'user_id.company_id.display_name', 'secret')
+ def _compute_qrcode(self):
+ # TODO: make "issuer" configurable through config parameter?
+ global_issuer = request and request.httprequest.host.split(':', 1)[0]
+ for w in self:
+ issuer = global_issuer or w.user_id.company_id.display_name
+ w.url = url = werkzeug.urls.url_unparse((
+ 'otpauth', 'totp',
+ werkzeug.urls.url_quote(f'{issuer}:{w.user_id.login}', safe=':'),
+ werkzeug.urls.url_encode({
+ 'secret': compress(w.secret),
+ 'issuer': issuer,
+ # apparently a lowercase hash name is anathema to google
+ # authenticator (error) and passlib (no token)
+ 'algorithm': ALGORITHM.upper(),
+ 'digits': DIGITS,
+ 'period': TIMESTEP,
+ }), ''
+ ))
+
+ data = io.BytesIO()
+ qrcode.make(url.encode(), box_size=4).save(data, optimise=True, format='PNG')
+ w.qrcode = base64.b64encode(data.getvalue()).decode()
+
+ @check_identity
+ def enable(self):
+ try:
+ c = int(compress(self.code))
+ except ValueError:
+ raise UserError(_("The verification code should only contain numbers"))
+ if self.user_id._totp_try_setting(self.secret, c):
+ self.secret = '' # empty it, because why keep it until GC?
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'type': 'success',
+ 'message': _("2-Factor authentication is now enabled."),
+ 'next': {'type': 'ir.actions.act_window_close'},
+ }
+ }
+ raise UserError(_('Verification failed, please double-check the 6-digit code'))
diff --git a/addons/auth_totp/wizard/auth_totp_wizard_views.xml b/addons/auth_totp/wizard/auth_totp_wizard_views.xml
new file mode 100644
index 00000000000..e5af62ebb6a
--- /dev/null
+++ b/addons/auth_totp/wizard/auth_totp_wizard_views.xml
@@ -0,0 +1,76 @@
+
+
+
+ auth_totp wizard
+ auth_totp.wizard
+
+
+
+
+