diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bf39cc7c99..c99717f3e4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,7 +93,7 @@ repos: - --color - --fix - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v5.0.0 hooks: - id: trailing-whitespace # exclude autogenerated files diff --git a/auth_sms/__manifest__.py b/auth_sms/__manifest__.py index 31927e24d5..431cbb5062 100644 --- a/auth_sms/__manifest__.py +++ b/auth_sms/__manifest__.py @@ -2,10 +2,11 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Two factor authentication via SMS", - "version": "10.0.1.0.0", + "version": "16.0.1.0.0", "author": "Therp BV,Odoo Community Association (OCA)", "license": "AGPL-3", - "category": "Extra Tools", + "category": "Tools", + "website": "https://github.com/OCA/server-auth", "summary": "Allow users to turn on two factor authentication via SMS", "depends": [ "mail", @@ -18,7 +19,7 @@ "views/sms_provider.xml", "views/res_users.xml", "security/ir_rule.xml", - "views/templates.xml", + "templates/template_code.xml", "security/ir.model.access.csv", ], } diff --git a/auth_sms/controllers/auth_sms.py b/auth_sms/controllers/auth_sms.py index 558d3970a4..f011634b1d 100644 --- a/auth_sms/controllers/auth_sms.py +++ b/auth_sms/controllers/auth_sms.py @@ -1,7 +1,6 @@ # Copyright 2019 Therp BV # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import base64 -import itertools import random from odoo import _, http @@ -25,97 +24,94 @@ def web_login(self, redirect=None, **kw): try: request.env["res.users"].sudo()._auth_sms_send(exception.user.id) except AccessDeniedSmsRateLimit: - return request.render( - "web.login", - dict( - request.params.copy(), - error=_("Rate limit for SMS exceeded"), - ), - ) - secret = self._auth_sms_generate_secret() - request.session["auth_sms.password"] = self._auth_sms_xor( - request.params["password"], - secret, - ) - return request.render( - "auth_sms.template_code", - dict( - request.params.copy(), - secret=base64.b64encode(secret), - redirect=redirect, - message=_("Please fill in the code sent to you by SMS"), - **kw - ), - ) + return self._show_rate_limit(redirect=None, **kw) + return self._show_sms_entry(redirect=None, **kw) + + def _show_rate_limit(self, redirect=None, **kw): + """User has requested to much sms codes in a short period.""" + # providers here as elsewhere are included in case auth_oauth is installed. + return request.render( + "web.login", + dict( + request.params.copy(), + providers=[], + error=_("Rate limit for SMS exceeded"), + ), + ) + + def _show_sms_entry(self, redirect=None, **kw): + """Show copy of login screen for sms entry.""" + # password will be stored, encrypted, in the session, while + # the secret will be send (and later retrieved) from the browser. + password_bytes = request.params["password"].encode("utf8") + secret = self._auth_sms_generate_secret() + encrypted_password = self._auth_sms_xor(password_bytes, secret) + request.session["auth_sms.password"] = encrypted_password + encoded_secret_string = base64.b64encode(secret).decode("utf8") + return request.render( + "auth_sms.template_code", + dict( + request.params.copy(), + secret=encoded_secret_string, + redirect=redirect, + providers=[], + message=_("Please fill in the code sent to you by SMS"), + **kw + ), + ) @http.route("/auth_sms/code", auth="none") def code(self, password=None, secret=None, redirect=None, **kw): + # IN this case the password argument really contains the sms code. request.session["auth_sms.code"] = password + encrypted_password = request.session["auth_sms.password"] + decoded_secret_bytes = base64.b64decode((secret or "").encode("utf8")) + decrypted_password = self._auth_sms_xor( + encrypted_password, decoded_secret_bytes + ) + request.params["password"] = decrypted_password.decode("utf8") + request.params["login"] = request.params["user_login"] try: - request.params["password"] = bytearray( - b - for b in self._auth_sms_xor( - request.session["auth_sms.password"] or bytearray(), - bytearray(base64.b64decode(secret or "")), - decode_input=False, - pad=False, - ) - # as we pad the password with null bytes, remove them here - if b - ).decode("utf8") - request.params["login"] = request.params["user_login"] return self.web_login( redirect=redirect, **dict(kw, password=request.params["password"]) ) except AccessDeniedWrongSmsCode: - del request.session["auth_sms.code"] - return request.render( - "auth_sms.template_code", - dict( - request.params.copy(), - secret=secret, - redirect=redirect, - databases=[], - error=_("Could not verify code"), - **kw - ), - ) + return self._show_wrong_sms_code(secret, redirect=None, **kw) - def _auth_sms_generate_secret(self): - """generate an OTP for storing the password in the session""" - return bytearray( - [ - random.randrange(256) - for dummy in range( - int( - request.env["ir.config_parameter"] - .sudo() - .get_param( - "auth_sms.otp_size", - 128, - ) - ) - ) - ] + def _show_wrong_sms_code(self, secret, redirect=None, **kw): + """Wrong sms code entered, user can try again.""" + del request.session["auth_sms.code"] + return request.render( + "auth_sms.template_code", + dict( + request.params.copy(), + secret=secret, + providers=[], + redirect=redirect, + databases=[], + error=_("Could not verify code"), + **kw + ), ) - def _auth_sms_xor(self, input_string, secret, decode_input=True, pad=True): - """xor input string with a pregenerated OTP, pad with 0""" - input_bytes = ( - decode_input and bytearray(input_string, encoding="utf8") or input_string - ) - return bytearray( - c ^ otp - for c, otp in zip( - itertools.chain( - input_bytes, - pad - and itertools.repeat( - 0, - len(secret) - len(input_bytes) % (len(secret) or 1), - ) - or [], - ), - itertools.cycle(secret), + def _auth_sms_generate_secret(self): + """Generate an OTP for storing the password in the session.""" + otp_size = int( + request.env["ir.config_parameter"] + .sudo() + .get_param( + "auth_sms.otp_size", + 128, ) ) + return bytes(bytearray([random.randrange(256) for dummy in range(otp_size)])) + + def _auth_sms_xor(self, password, secret): + """Xor password with secret, to encrypt or decrypt password. + + password and secret should both be byte strings. + """ + assert len(secret) >= len(password) + assert isinstance(password, bytes) + assert isinstance(secret, bytes) + return bytes(bytearray(c ^ otp for c, otp in zip(password, secret))) diff --git a/auth_sms/exceptions.py b/auth_sms/exceptions.py index a603fcfe38..dd164b81b4 100644 --- a/auth_sms/exceptions.py +++ b/auth_sms/exceptions.py @@ -1,11 +1,11 @@ -# Copyright 2019 Therp BV +# Copyright 20192024 Therp BV . # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). class AccessDeniedNoSmsCode(Exception): def __init__(self, user, message=None): self.user = user - super(AccessDeniedNoSmsCode, self).__init__(message) + super().__init__(message) class AccessDeniedWrongSmsCode(Exception): diff --git a/auth_sms/models/sms_provider.py b/auth_sms/models/sms_provider.py index 696de48324..2f6d042764 100644 --- a/auth_sms/models/sms_provider.py +++ b/auth_sms/models/sms_provider.py @@ -67,6 +67,7 @@ def _send_sms_messagebird(self, number, text, **kwargs): "recipients": number, "body": text, }, + timeout=60, ).json() _logger.debug(result) if result.get("errors"): diff --git a/auth_sms/static/description/index.html b/auth_sms/static/description/index.html index 4ccb6d89c6..349c0bfe96 100644 --- a/auth_sms/static/description/index.html +++ b/auth_sms/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { @@ -461,9 +460,7 @@

Other credits

Maintainers

This module is maintained by the OCA.

- -Odoo Community Association - +Odoo Community Association

OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.

diff --git a/auth_sms/views/templates.xml b/auth_sms/templates/template_code.xml similarity index 93% rename from auth_sms/views/templates.xml rename to auth_sms/templates/template_code.xml index 9da2af4aa4..072d3fb7b2 100644 --- a/auth_sms/views/templates.xml +++ b/auth_sms/templates/template_code.xml @@ -1,8 +1,9 @@ -