From bcd91c44c7faf223463b444472e1a56e26d8db3e Mon Sep 17 00:00:00 2001 From: Maxime Gasselin Date: Wed, 6 May 2020 16:30:52 +0200 Subject: [PATCH] [banquepopulaire] repair login for bpaca "Phase" bpcesta params in authorize url is not necessary anymore for "part" bpaca user. But for the other cases the param is still required. The phase information seems to be in js file and the value is not hardcoded. That's why we must be vigilant if other subwebsites migrate to this version. Moreover in bpaca the good user status is required. We can get the information in as-ex-ano-groupe.banquepopulaire.fr API. --- modules/banquepopulaire/browser.py | 89 +++++++++++++++++++++++++----- modules/banquepopulaire/pages.py | 19 +++++++ 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/modules/banquepopulaire/browser.py b/modules/banquepopulaire/browser.py index 1d655e7ba0..98ad0db25f 100644 --- a/modules/banquepopulaire/browser.py +++ b/modules/banquepopulaire/browser.py @@ -19,6 +19,7 @@ from __future__ import unicode_literals +import json import re from uuid import uuid4 @@ -45,7 +46,7 @@ LineboursePage, AlreadyLoginPage, InvestmentPage, NewLoginPage, JsFilePage, AuthorizePage, LoginTokensPage, VkImagePage, AuthenticationMethodPage, AuthenticationStepPage, CaissedepargneVirtKeyboard, - AccountsNextPage, GenericAccountsPage, + AccountsNextPage, GenericAccountsPage, InfoTokensPage, ) from .document_pages import BasicTokenPage, SubscriberPage, SubscriptionsPage, DocumentsPage @@ -111,6 +112,8 @@ class BanquePopulaire(LoginBrowser): js_file = URL(r'https://[^/]+/se-connecter/main-.*.js$', JsFilePage) authorize = URL(r'https://www.as-ex-ath-groupe.banquepopulaire.fr/api/oauth/v2/authorize', AuthorizePage) login_tokens = URL(r'https://www.as-ex-ath-groupe.banquepopulaire.fr/api/oauth/v2/consume', LoginTokensPage) + info_tokens = URL(r'https://www.as-ex-ano-groupe.banquepopulaire.fr/api/oauth/token', InfoTokensPage) + user_info = URL(r'https://www.rs-ex-ano-groupe.banquepopulaire.fr/bapi/user/v1/users/identificationRouting', InfoTokensPage) authentication_step = URL( r'https://www.icgauth.banquepopulaire.fr/dacsrest/api/v1u0/transaction/(?P[^/]+)/step', AuthenticationStepPage ) @@ -205,6 +208,7 @@ class BanquePopulaire(LoginBrowser): documents_page = URL(r'/api-bp/wapi/2.0/abonnes/current/documents/recherche-avancee', DocumentsPage) def __init__(self, website, *args, **kwargs): + self.website = website self.BASEURL = 'https://%s' % website # this url is required because the creditmaritime abstract uses an other url if 'cmgo.creditmaritime' in self.BASEURL: @@ -301,24 +305,79 @@ def do_new_login(self): client_id = self.page.get_client_id() nonce = self.page.get_nonce() # Hardcoded in their js... + data = { + 'grant_type': 'client_credentials', + 'client_id': self.page.get_user_info_client_id(), + 'scope': '', + } + + # The 2 followings requests are needed in order to get + # user type (part, ent and pro) + self.info_tokens.go(data=data) + + headers = {'Authorization': 'Bearer %s' % self.page.get_access_token()} + data = { + 'characteristics': { + 'iTEntityType': { + 'code': '03', # 03 for BP and 02 for CE + 'label': 'BP', + }, + 'userCode': self.username, + 'bankId': cdetab, + 'subscribeTypeItems': [], + } + } + self.user_info.go(headers=headers, json=data) + self.user_type = self.page.get_user_type() + # On the website, this sends back json because of the header # 'Accept': 'applcation/json'. If we do not add this header, we # instead have a form that we can directly send to complete # the login. - self.authorize.go( - params={ - 'nonce': nonce, - 'scope': '', - 'response_type': 'id_token token', - 'response_mode': 'form_post', - 'cdetab': cdetab, - 'login_hint': self.username.upper(), - 'display': 'page', - 'client_id': client_id, - 'claims': '{"userinfo":{"cdetab":null,"authMethod":null,"authLevel":null},"id_token":{"auth_time":{"essential":true},"last_login":null}}', - 'bpcesta': '{"csid":"%s","typ_app":"rest","enseigne":"bp","typ_sp":"out-band","typ_act":"auth","snid":"%s","cdetab":"%s","typ_srv":"part","phase":"1"}' % (str(uuid4()), 123456, cdetab), + bpcesta = { + 'csid': str(uuid4()), + 'typ_app': 'rest', + 'enseigne': 'bp', + 'typ_sp': 'out-band', + 'typ_act': 'auth', + 'snid': '123456', + 'cdetab': cdetab, + 'typ_srv': self.user_type, + } + claims = { + 'userinfo': { + 'cdetab': None, + 'authMethod': None, + 'authLevel': None }, - ) + 'id_token': { + 'auth_time': { + 'essential': True, + }, + 'last_login': None, + }, + } + # We need to avoid to add "phase":"1" for part in bpaca website + # Maybe it will be the case for other websites + # The phase information seems to be in js file and the value is not hardcode + if self.website not in ('www.ibps.bpaca.banquepopulaire.fr', ) or self.user_type != 'part': + # Here if we don't add phase":"1" we would get false wrongpass + bpcesta['phase'] = "1" + + params={ + 'nonce': nonce, + 'scope': '', + 'response_type': 'id_token token', + 'response_mode': 'form_post', + 'cdetab': cdetab, + 'login_hint': self.username.upper(), + 'display': 'page', + 'client_id': client_id, + 'claims': json.dumps(claims), + 'bpcesta': json.dumps(bpcesta), + } + + self.authorize.go(params=params) self.page.send_form() self.page.check_errors(feature='login') @@ -373,7 +432,7 @@ def do_new_login(self): 'token_type': 'Bearer', 'grant_type': 'implicit flow', 'NameId': self.username.upper(), - 'Segment': 'part', + 'Segment': self.user_type, 'scopes': '', 'expires_in': expires_in, }, diff --git a/modules/banquepopulaire/pages.py b/modules/banquepopulaire/pages.py index c2daf4ec02..ec8b29c7cf 100644 --- a/modules/banquepopulaire/pages.py +++ b/modules/banquepopulaire/pages.py @@ -321,6 +321,9 @@ class JsFilePage(AbstractPage): PARENT_URL = 'js_file' BROWSER_ATTR = 'package.browser.CaisseEpargne' + def get_user_info_client_id(self): + return Regexp(pattern=r'anonymous:{clientId:"([^"]+)"').filter(self.text) + class AuthorizePage(AbstractPage): PARENT = 'caissedepargne' @@ -337,6 +340,22 @@ def get_expires_in(self): return Dict('parameters/expires_in')(self.doc) +class InfoTokensPage(JsonPage): + def get_access_token(self): + return Dict('access_token')(self.doc) + + def get_user_type(self): + user_subscription = Dict('characteristics/subscribeTypeItems/0/label')(self.doc) + user_types = { + 'abonnement Particulier': 'part', + 'abonnement Personne Morale': 'ent', + 'abonnement EI (pro)': 'pro', + } + user_type = user_types.get(user_subscription) + assert user_type, "%s user type is not yet handle" % user_subscription + return user_type + + class VkImagePage(AbstractPage): PARENT = 'caissedepargne' PARENT_URL = 'vk_image' -- GitLab