Files
odoo_source/addons/auth_totp/tests/test_totp.py
T
Pierre Paridans 7df7ecae9b [FIX] auth_totp,web: TOTP login from mobile apps
Since PR odoo/odoo#78857 , the TOTP authentication support is broken
when used inside either Android or iOS mobile apps.

Due to our inability to update the iOS app (following review from
Apple), this commit aims at restoring the bare minimum requirements to
make the current mobile apps (specially iOS but also Android)
authentication workflow works.

As extended explanation:
- Set-Cookie header is expected to be sent even when session_id hasn't
  changed (iOS specific).
- Successful credentials check on `/web/session/authenticate` expect a
  successful response with a result containing `uid` set to `null` to
  mark the need of an additional totp handshake (both platforms).

closes odoo/odoo#85463

Signed-off-by: Julien Castiaux <juc@odoo.com>
2022-02-26 13:41:23 +00:00

118 lines
4.2 KiB
Python

import json
import time
from xmlrpc.client import Fault
from passlib.totp import TOTP
from odoo import http
from odoo.exceptions import AccessDenied
from odoo.service import common as auth, model
from odoo.tests import tagged, HttpCase, get_db_name
from odoo.tools import mute_logger
from ..controllers.home import Home
@tagged('post_install', '-at_install')
class TestTOTP(HttpCase):
def setUp(self):
super().setUp()
totp = None
# might be possible to do client-side using `crypto.subtle` instead of
# this horror show, but requires working on 64b integers, & BigInt is
# significantly less well supported than crypto
def totp_hook(self, secret=None):
nonlocal totp
if totp is None:
totp = TOTP(secret)
if secret:
return totp.generate().token
else:
# on check, take advantage of window because previous token has been
# "burned" so we can't generate the same, but tour is so fast
# we're pretty certainly within the same 30s
return totp.generate(time.time() + 30).token
# because not preprocessed by ControllerType metaclass
totp_hook.routing_type = 'json'
self.env['ir.http']._clear_routing_map()
# patch Home to add test endpoint
Home.totp_hook = http.route('/totphook', type='json', auth='none')(totp_hook)
# remove endpoint and destroy routing map
@self.addCleanup
def _cleanup():
del Home.totp_hook
self.env['ir.http']._clear_routing_map()
def test_totp(self):
# 1. Enable 2FA
self.start_tour('/web', 'totp_tour_setup', login='demo')
# 2. Verify that RPC is blocked because 2FA is on.
self.assertFalse(
self.xmlrpc_common.authenticate(get_db_name(), 'demo', 'demo', {}),
"Should not have returned a uid"
)
self.assertFalse(
self.xmlrpc_common.authenticate(get_db_name(), 'demo', 'demo', {'interactive': True}),
'Trying to fake the auth type should not work'
)
uid = self.env.ref('base.user_demo').id
with self.assertRaisesRegex(Fault, r'Access Denied'):
self.xmlrpc_object.execute_kw(
get_db_name(), uid, 'demo',
'res.users', 'read', [uid, ['login']]
)
# 3. Check 2FA is required
self.start_tour('/', 'totp_login_enabled', login=None)
# 4. Check 2FA is not requested on saved device and disable it
self.start_tour('/', 'totp_login_device', login=None)
# 5. Finally, check that 2FA is in fact disabled
self.start_tour('/', 'totp_login_disabled', login=None)
# 6. Check that rpc is now re-allowed
uid = self.xmlrpc_common.authenticate(get_db_name(), 'demo', 'demo', {})
self.assertEqual(uid, self.env.ref('base.user_demo').id)
[r] = self.xmlrpc_object.execute_kw(
get_db_name(), uid, 'demo',
'res.users', 'read', [uid, ['login']]
)
self.assertEqual(r['login'], 'demo')
def test_totp_administration(self):
self.start_tour('/web', 'totp_tour_setup', login='demo')
self.start_tour('/web', 'totp_admin_disables', login='admin')
self.start_tour('/', 'totp_login_disabled', login=None)
@mute_logger('odoo.http')
def test_totp_authenticate(self):
"""
Ensure we don't leak the session info from an half-logged-in
user.
"""
self.start_tour('/web', 'totp_tour_setup', login='demo')
self.url_open('/web/session/logout')
headers = {
"Content-Type": "application/json",
}
payload = {
"jsonrpc": "2.0",
"method": "call",
"id": 0,
"params": {
"db": get_db_name(),
"login": "demo",
"password": "demo",
"context": {},
},
}
response = self.url_open("/web/session/authenticate", data=json.dumps(payload), headers=headers)
data = response.json()
self.assertEqual(data['result']['uid'], None)