@@ -958,6 +958,7 @@ def session_authenticate(self, login=None, password=None):
958958 'password' : password or getpass (f"Password for { login !r} : " ),
959959 }
960960 self .session_info = self .client ._authenticate_session (** params )
961+ print (f'Session authenticated for { login !r} ' if self .session_info ['uid' ] else 'Failed' )
961962
962963 def session_destroy (self ):
963964 """Terminate current Webclient session."""
@@ -1124,9 +1125,44 @@ def _authenticate(self, db, login, password):
11241125 return info
11251126
11261127 def _authenticate_session (self , db , login , password ):
1127- info = self .web_session .authenticate (db = db , login = login , password = password )
1128+ try :
1129+ info = self .web_session .authenticate (db = db , login = login , password = password )
1130+ except ServerError as exc :
1131+ # Ignore: odoo.exceptions.AccessDenied
1132+ if exc .args [0 ]['code' ] != 200 :
1133+ raise
1134+ return {'uid' : None }
1135+ if self .version_info > 14.0 and info ['uid' ] is None : # Is it 2FA?
1136+ info = self ._authenticate_totp (db , login , password )
11281137 return info
11291138
1139+ def _authenticate_totp (self , db , login , password ):
1140+ headers = {'User-Agent' : 'Mozilla/5.0 (X11)' }
1141+
1142+ # 1. Get CSRF token
1143+ rv = self ._post (f'{ self .web ._server } web' , method = 'GET' , headers = headers )
1144+ csrf = re .search (r'csrf_token: "(\w+)"' , rv ).group (1 )
1145+
1146+ # 2. Login
1147+ params = {'csrf_token' : csrf , 'db' : db , 'login' : login , 'password' : password }
1148+ rv = self ._post (f'{ self .web ._server } web/login' , data = params , headers = headers )
1149+
1150+ for retry in range (4 ):
1151+ # 3. Parse 'session_info'
1152+ session_info = json .loads (re .search (r'odoo.__session_info__ = (.*);' , rv ).group (1 ))
1153+ if session_info ['uid' ] or 'totp_token' not in rv or retry == 3 :
1154+ break
1155+ if retry :
1156+ print ('Verification failed' )
1157+
1158+ # 4. Ask TOTP code
1159+ token = getpass (f"Authentication Code for { login !r} (2FA 6-digits): " )
1160+
1161+ # 5. Submit TOTP
1162+ params = {'csrf_token' : csrf , 'totp_token' : token , 'remember' : 1 }
1163+ rv = self ._post (f'{ self .web ._server } web/login/totp' , data = params , headers = headers )
1164+ return session_info
1165+
11301166 def _login (self , user , password = None , database = None ):
11311167 """Switch `user` and (optionally) `database`.
11321168
@@ -1266,22 +1302,25 @@ def _call_kw(self, model, method, args, kw=None):
12661302 def _set_http_session (self ):
12671303 self ._http_session = requests .Session ()
12681304
1269- def _post (self , url , * , data = None , json = None , headers = None , ** kw ):
1270- resp = self ._http_session .post ( url , data = data , json = json , headers = headers , ** kw )
1271- return resp .json () if json else resp .text
1305+ def _post (self , url , * , method = 'POST' , data = None , json = None , headers = None , ** kw ):
1306+ resp = self ._http_session .request ( method , url , data = data , json = json , headers = headers , ** kw )
1307+ return resp .text if json is None else resp .json ()
12721308
12731309 else : # urllib.request
12741310 def _set_http_session (self ):
12751311 self ._http_session = build_opener (HTTPCookieProcessor (), HTTPSHandler (context = http_context ))
12761312
1277- def _post (self , url , * , data = None , json = None , headers = None , __json = json , ** kw ):
1313+ def _post (self , url , * , method = 'POST' , data = None , json = None , headers = None , __json = json , ** kw ):
12781314 headers = dict (headers or ())
1279- if json :
1315+ if json is not None :
12801316 headers .setdefault ('Content-Type' , 'application/json' )
1281- data = __json .dumps (json ).encode ('ascii' ) if json else urlencode (data ).encode ('utf-8' )
1317+ if method != 'GET' :
1318+ data = __json .dumps (json ).encode ('ascii' ) if json else urlencode (data ).encode ('utf-8' )
1319+ elif data is not None :
1320+ url , data = f'{ url } ?{ urlencode (data )} ' , None
12821321 request = Request (url , data = data , headers = headers )
12831322 resp = self ._http_session .open (request )
1284- return __json . load (resp ) if json else resp . read ( )
1323+ return str (resp . read (), 'utf-8' ) if json is None else __json . load ( resp )
12851324
12861325
12871326class BaseModel :
0 commit comments