From 765b23950d6c7ebea7bd5f04be4a913c2b914980 Mon Sep 17 00:00:00 2001 From: Guillaume Risbourg Date: Wed, 19 Feb 2020 10:57:27 +0100 Subject: [PATCH] [caissedepargne] Handle CloudCard on new_recipients CloudCard (aka Secur'Pass) is a method that asks for an AppValidation when adding new recipients or making transfer when not on the Caisse d'Epargne application. --- modules/caissedepargne/browser.py | 104 +++++++++++++++++++++++------- modules/caissedepargne/pages.py | 14 +++- 2 files changed, 93 insertions(+), 25 deletions(-) diff --git a/modules/caissedepargne/browser.py b/modules/caissedepargne/browser.py index 2259898184..eb20c70023 100644 --- a/modules/caissedepargne/browser.py +++ b/modules/caissedepargne/browser.py @@ -19,6 +19,7 @@ from __future__ import unicode_literals +import time import re import datetime from hashlib import sha256 @@ -41,7 +42,7 @@ from weboob.browser.exceptions import BrowserHTTPNotFound, ClientError, ServerError from weboob.exceptions import ( BrowserIncorrectPassword, BrowserUnavailable, BrowserHTTPError, BrowserPasswordExpired, - AuthMethodNotImplemented, + AuthMethodNotImplemented, AppValidation, AppValidationExpired, ) from weboob.tools.capabilities.bank.transactions import ( sorted_transactions, FrenchTransaction, keep_only_card_transactions, @@ -62,6 +63,7 @@ SubscriptionPage, CreditCooperatifMarketPage, UnavailablePage, CardsPage, CardsComingPage, CardsOldWebsitePage, TransactionPopupPage, OldLeviesPage, NewLeviesPage, NewLoginPage, JsFilePage, AuthorizePage, AuthenticationMethodPage, VkImagePage, AuthenticationStepPage, LoginTokensPage, + AppValidationPage, ) from .transfer_pages import CheckingPage, TransferListPage @@ -160,6 +162,7 @@ class CaisseEpargne(LoginBrowser, StatesMixin): # https://www.icgauth.caisse-epargne.fr/dacstemplate-SOL/_12579/index.html?transactionID=CtxDACSP[a-f0-9]+ validation_option = URL(r'https://(?Pwww.icgauth.[^/]+)/dacstemplate-SOL/(?:[^/]+/)?index.html\?transactionID=.*', ValidationPageOption) sms = URL(r'https://(?Pwww.icgauth.[^/]+)/dacswebssoissuer/AuthnRequestServlet', SmsPage) + app_validation = URL(r'https://(?Pwww.icgauth.[^/]+)/dacsrest/WaitingCallbackHandler', AppValidationPage) account_login = URL(r'/authentification/manage\?step=account&identifiant=(?P.*)&account=(?P.*)', LoginPage) loading = URL(r'https://.*/CreditConso/ReroutageCreditConso.aspx', LoadingPage) @@ -227,7 +230,7 @@ class CaisseEpargne(LoginBrowser, StatesMixin): __states__ = ( 'BASEURL', 'multi_type', 'typeAccount', 'is_cenet_website', 'recipient_form', - 'is_send_sms', 'otp_validation', + 'is_send_sms', 'is_app_validation', 'otp_validation', ) # Accounts managed in life insurance space (not in linebourse) @@ -298,7 +301,7 @@ def load_state(self, state): if state.get('expire') and parser.parse(state['expire']) < datetime.datetime.now(): return self.logger.info('State expired, not reloading it from storage') - transfer_states = ('recipient_form', 'is_send_sms', 'otp_validation') + transfer_states = ('recipient_form', 'is_app_validation', 'is_send_sms', 'otp_validation') for transfer_state in transfer_states: if transfer_state in state and state[transfer_state] is not None: @@ -507,7 +510,7 @@ def do_otp_sms_authentication(self, **params): * do not forget to set `otp_sms` params Parameters: - otp_sms (str): the OTP recieved by SMS + otp_sms (str): the OTP received by SMS """ assert self.otp_validation assert 'otp_sms' in params @@ -529,7 +532,52 @@ def do_otp_sms_authentication(self, **params): self.otp_validation = None def do_cloudcard_authentication(self, **params): - raise AuthMethodNotImplemented() + """ Second step of cloudcard authentication validation + + This method check the application validation status. + Warning: + * need to be used through `do_authentication_validation` method + in order to handle authentication response + * do not forget to use the first part to have all form information + """ + assert self.otp_validation + + timeout = time.time() + 300.0 + referer_url = self.authentication_method_page.build( + domain=self.otp_validation['domain'], + validation_id=self.otp_validation['validation_id'], + ) + + while time.time() < timeout: + self.app_validation.go( + domain=self.otp_validation['domain'], + headers={'Referer': referer_url}, + ) + status = self.page.get_status() + # The status is 'valid' even when the user cancels it on + # the application. The `authentication_step` will return + # AUTHENTICATION_CANCELED in its response status. + if status == 'valid': + self.authentication_step.go( + domain=self.otp_validation['domain'], + validation_id=self.otp_validation['validation_id'], + json={ + 'validate': { + self.otp_validation['validation_unit_id']: [{ + 'id': self.otp_validation['id'], + 'type': 'CLOUDCARD', + }], + }, + }, + ) + break + + assert status == 'progress', 'Unhandled CloudCard status : "%s"' % status + time.sleep(2) + else: + raise AppValidationExpired() + + self.otp_validation = None def do_vk_authentication(self, **params): """ Authentication with virtual keyboard @@ -1301,6 +1349,7 @@ def pre_transfer(self, account): @need_login def init_transfer(self, account, recipient, transfer): self.is_send_sms = False + self.is_app_validation = False self.pre_transfer(account) # Warning: this may send a sms or an app validation @@ -1312,8 +1361,6 @@ def init_transfer(self, account, recipient, transfer): if self.otp_validation['type'] == 'CLOUDCARD': raise AuthMethodNotImplemented() - self.is_send_sms = True - raise TransferStep( transfer, Value( @@ -1390,12 +1437,18 @@ def new_recipient(self, recipient, **params): if 'sms_password' in params: return self.end_sms_recipient(recipient, **params) - if 'otp_sms' in params: - self.do_authentication_validation( - authentication_method='SMS', - otp_sms=params['otp_sms'], - feature='recipient' - ) + if 'otp_sms' in params or 'resume' in params: + if 'otp_sms' in params: + self.do_authentication_validation( + authentication_method='SMS', + otp_sms=params['otp_sms'], + feature='recipient' + ) + else: + self.do_authentication_validation( + authentication_method='CLOUDCARD', + feature='recipient' + ) if self.authent.is_here(): self.page.go_on() @@ -1415,17 +1468,22 @@ def new_recipient(self, recipient, **params): if self.validation_option.is_here(): self.get_auth_mechanisms_validation_info() - if self.otp_validation['type'] == 'CLOUDCARD': - raise AuthMethodNotImplemented() - - self.is_send_sms = True - raise AddRecipientStep( - self.get_recipient_obj(recipient), - Value( - 'otp_sms', - label='Veuillez renseigner le mot de passe unique qui vous a été envoyé par SMS dans le champ réponse.' + recipient_obj = self.get_recipient_obj(recipient) + if self.otp_validation['type'] == 'SMS': + self.is_send_sms = True + raise AddRecipientStep( + recipient_obj, + Value( + 'otp_sms', + label='Veuillez renseigner le mot de passe unique qui vous a été envoyé par SMS dans le champ réponse.' + ) + ) + elif self.otp_validation['type'] == 'CLOUDCARD': + self.is_app_validation = True + raise AppValidation( + resource=recipient_obj, + message="Veuillez valider l'ajout de bénéficiaire sur votre application mobile." ) - ) # pro add recipient. elif self.page.need_auth(): diff --git a/modules/caissedepargne/pages.py b/modules/caissedepargne/pages.py index 0ca7402354..26d55086f2 100644 --- a/modules/caissedepargne/pages.py +++ b/modules/caissedepargne/pages.py @@ -29,7 +29,10 @@ from datetime import datetime from lxml import html -from weboob.browser.pages import LoggedPage, HTMLPage, JsonPage, pagination, FormNotFound, RawPage +from weboob.browser.pages import ( + LoggedPage, HTMLPage, JsonPage, pagination, + FormNotFound, RawPage, XMLPage, +) from weboob.browser.elements import ItemElement, method, ListElement, TableElement, SkipItem, DictElement from weboob.browser.filters.standard import ( Date, CleanDecimal, Regexp, CleanText, Env, Upper, @@ -41,7 +44,7 @@ Account, Loan, AccountOwnership, Transfer, TransferBankError, TransferInvalidOTP, Recipient, AddRecipientBankError, RecipientInvalidOTP, - Emitter, EmitterNumberType, + Emitter, EmitterNumberType, AddRecipientError, ) from weboob.capabilities.wealth import Investment from weboob.capabilities.bill import DocumentTypes, Subscription, Document @@ -149,6 +152,8 @@ def recipient_errors(self, error): if error == 'FAILED_AUTHENTICATION': # For the moment, only otp sms is handled raise RecipientInvalidOTP(message="Le code SMS que vous avez renseigné n'est pas valide") + elif error == 'AUTHENTICATION_CANCELED': + raise AddRecipientError(message="L'ajout a été annulée via l'application mobile.") def check_errors(self, feature): if 'response' in self.doc: @@ -1966,6 +1971,11 @@ class CanceledAuth(Exception): pass +class AppValidationPage(LoggedPage, XMLPage): + def get_status(self): + return CleanText('//response/status')(self.doc) + + class SmsPage(LoggedPage, HTMLPage): def on_load(self): error = CleanText('//p[@class="warning_trials_before"]')(self.doc) -- GitLab