Add two factor authentication for boursorama module

This commit is contained in:
Gabriel 2012-12-05 18:11:22 +01:00 committed by Romain Bignon
commit ee4f3793a1
4 changed files with 196 additions and 9 deletions

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright(C) 2012 Gabriel Serme
# Copyright(C) 2011 Gabriel Kerneis # Copyright(C) 2011 Gabriel Kerneis
# Copyright(C) 2010-2011 Jocelyn Jaubert # Copyright(C) 2010-2011 Jocelyn Jaubert
# #
@ -24,7 +25,7 @@ from __future__ import with_statement
from weboob.capabilities.bank import ICapBank, AccountNotFound from weboob.capabilities.bank import ICapBank, AccountNotFound
from weboob.tools.backend import BaseBackend, BackendConfig from weboob.tools.backend import BaseBackend, BackendConfig
from weboob.tools.value import ValueBackendPassword from weboob.tools.value import ValueBackendPassword, ValueBool, Value
from .browser import Boursorama from .browser import Boursorama
@ -40,12 +41,17 @@ class BoursoramaBackend(BaseBackend, ICapBank):
LICENSE = 'AGPLv3+' LICENSE = 'AGPLv3+'
DESCRIPTION = u'Boursorama French bank website' DESCRIPTION = u'Boursorama French bank website'
CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False), CONFIG = BackendConfig(ValueBackendPassword('login', label='Account ID', masked=False),
ValueBackendPassword('password', label='Password')) ValueBackendPassword('password', label='Password'),
ValueBool('enable_twofactors', label='Send validation sms', default=False),
Value('device', label='Device name', regexp='\w*'),)
BROWSER = Boursorama BROWSER = Boursorama
def create_default_browser(self): def create_default_browser(self):
return self.create_browser(self.config['login'].get(), return self.create_browser(
self.config['password'].get()) self.config["device"].get()
, self.config["enable_twofactors"].get()
, self.config['login'].get()
, self.config['password'].get())
def iter_accounts(self): def iter_accounts(self):
for account in self.browser.get_accounts_list(): for account in self.browser.get_accounts_list():

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright(C) 2012 Gabriel Serme
# Copyright(C) 2011 Gabriel Kerneis # Copyright(C) 2011 Gabriel Kerneis
# Copyright(C) 2010-2011 Jocelyn Jaubert # Copyright(C) 2010-2011 Jocelyn Jaubert
# #
@ -20,25 +21,33 @@
from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword from weboob.tools.browser import BaseBrowser, BrowserIncorrectPassword
from .pages import LoginPage, AccountsList, AccountHistory, UpdateInfoPage from .pages import LoginPage, AccountsList, AccountHistory, UpdateInfoPage, AuthenticationPage
__all__ = ['Boursorama'] __all__ = ['Boursorama']
class BrowserIncorrectAuthenticationCode(BrowserIncorrectPassword):
pass
class Boursorama(BaseBrowser): class Boursorama(BaseBrowser):
DOMAIN = 'www.boursorama.com' DOMAIN = 'www.boursorama.com'
PROTOCOL = 'https' PROTOCOL = 'https'
CERTHASH = '74429081f489cb723a82171a94350913d42727053fc86cf5bf5c3d65d39ec449' CERTHASH = '74429081f489cb723a82171a94350913d42727053fc86cf5bf5c3d65d39ec449'
ENCODING = None # refer to the HTML encoding ENCODING = None # refer to the HTML encoding
PAGES = { PAGES = {
'.*/connexion/securisation/index.phtml': AuthenticationPage,
'.*connexion.phtml.*': LoginPage, '.*connexion.phtml.*': LoginPage,
'.*/comptes/synthese.phtml': AccountsList, '.*/comptes/synthese.phtml': AccountsList,
'.*/mouvements.phtml.*': AccountHistory, '.*/comptes/banque/detail/mouvements.phtml.*': AccountHistory,
'.*/date_anniversaire.phtml.*': UpdateInfoPage, '.*/date_anniversaire.phtml.*': UpdateInfoPage,
} }
def __init__(self, *args, **kwargs): def __init__(self, device="weboob", enable_twofactors=False
, *args, **kwargs):
self.device = device
self.enable_twofactors = enable_twofactors
BaseBrowser.__init__(self, *args, **kwargs) BaseBrowser.__init__(self, *args, **kwargs)
def home(self): def home(self):
@ -47,11 +56,25 @@ class Boursorama(BaseBrowser):
def is_logged(self): def is_logged(self):
return not self.is_on_page(LoginPage) return not self.is_on_page(LoginPage)
def handle_authentication(self):
if self.is_on_page(AuthenticationPage):
if self.enable_twofactors:
self.page.authenticate(self.device)
else:
print \
"""Boursorama - activate the two factor authentication in boursorama config."""\
""" You will receive SMS code but are limited in request per day (around 15)"""
def login(self): def login(self):
assert isinstance(self.username, basestring) assert isinstance(self.username, basestring)
assert isinstance(self.password, basestring) assert isinstance(self.password, basestring)
assert isinstance(self.device, basestring)
assert isinstance(self.enable_twofactors, bool)
assert self.password.isdigit() assert self.password.isdigit()
#for debug, save requested pages to tmp dir
#self.SAVE_RESPONSES = True
if not self.is_on_page(LoginPage): if not self.is_on_page(LoginPage):
self.location('https://' + self.DOMAIN + '/connexion.phtml') self.location('https://' + self.DOMAIN + '/connexion.phtml')
@ -60,6 +83,22 @@ class Boursorama(BaseBrowser):
if self.is_on_page(LoginPage): if self.is_on_page(LoginPage):
raise BrowserIncorrectPassword() raise BrowserIncorrectPassword()
#after login, we might be redirected to the two factor
#authentication page
#print "handle authentication"
self.handle_authentication()
self.location('/comptes/synthese.phtml', no_login=True)
#if the login was correct but authentication code failed,
#we need to verify if bourso redirect us to login page or authentication page
if self.is_on_page(LoginPage):
#print "not correct after handling authentication"
raise BrowserIncorrectAuthenticationCode()
#print "login over"
def get_accounts_list(self): def get_accounts_list(self):
if not self.is_on_page(AccountsList): if not self.is_on_page(AccountsList):
self.location('/comptes/synthese.phtml') self.location('/comptes/synthese.phtml')

View file

@ -23,6 +23,7 @@ from .account_history import AccountHistory
from .accounts_list import AccountsList from .accounts_list import AccountsList
from .login import LoginPage, UpdateInfoPage from .login import LoginPage, UpdateInfoPage
from .two_authentication import AuthenticationPage
class AccountPrelevement(AccountsList): class AccountPrelevement(AccountsList):
pass pass
@ -31,4 +32,5 @@ __all__ = ['LoginPage',
'AccountsList', 'AccountsList',
'AccountHistory', 'AccountHistory',
'UpdateInfoPage', 'UpdateInfoPage',
'AuthenticationPage',
] ]

View file

@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
# Copyright(C) 2012 Gabriel Serme
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.
from weboob.tools.browser import BasePage, BrowserIncorrectPassword
import urllib2
import re
__all__ = ['AuthenticationPage']
class BrowserAuthenticationCodeMaxLimit(BrowserIncorrectPassword):
pass
def write_debug(string, fi):
f = open(fi, "w")
f.write(string)
class AuthenticationPage(BasePage):
MAX_LIMIT = "vous avez atteint le nombre maximum "\
"d'utilisation de l'authentification forte."
def on_loaded(self):
pass
def authenticate(self, device):
"""This function simulates the registration of a device on
boursorama two factor authentification web page.
I
@param device device name to register
@exception BrowserAuthenticationCodeMaxLimit when daily limit is consumed
@exception BrowserIncorrectAuthenticationCode when code is not correct
"""
DOMAIN = self.browser.DOMAIN
SECURE_PAGE = "https://www.boursorama.com/comptes/connexion/securisation/index.phtml"
REFERER = SECURE_PAGE
#print "Need to authenticate for device", device
#print "Domain information", DOMAIN
url = "https://%s/ajax/banque/otp.phtml?org=%s&alertType=10100" % (DOMAIN, REFERER)
#print url
headers = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows "
"NT 5.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8"
" GTB7.1 (.NET CLR 3.5.30729)",
"Referer": REFERER,
}
headers_ajax = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows "
"NT 5.1; en-US; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8"
" GTB7.1 (.NET CLR 3.5.30729)",
"Accept": "application/json",
"X-Requested-With": "XMLHttpRequest",
"X-Request": "JSON",
"X-Brs-Xhr-Request": "true",
"X-Brs-Xhr-Schema": "DATA+OUT",
"Referer": REFERER,
}
req = urllib2.Request(url, headers=headers_ajax)
response = self.browser.open(req)
#extrat authentication token from response (in form)
info = response.read()
#write_debug(info, "step1.html")
regex = re.compile(r"vous avez atteint le nombre maximum d'utilisation de l'authentification forte.")
r = regex.search(info)
if r:
print "Boursorama - Vous avez atteint le nombre maximum d'utilisation de l'authentification forte"
raise BrowserAuthenticationCodeMaxLimit()
#print "Response from initial request,", len(info), response.info()
regex = re.compile(r"name=\\\"authentificationforteToken\\\" "
r"value=\\\"(?P<value>\w*?)\\\"")
r = regex.search(info)
token = r.group('value')
#print "Extracted token", token
#self.print_cookies()
#step2
url = "https://" + DOMAIN + "/ajax/banque/otp.phtml"
data = "authentificationforteToken=%s&authentificationforteStep=start&alertType=10100&org=%s&validate=" % (token, REFERER)
req = urllib2.Request(url, data, headers_ajax)
response = self.browser.open(req)
#info = response.read()
#print "after asking to send token authentification" \
# ,len(info), response.info()
#write_debug(info, "step2.html")
#self.print_cookies()
pin = raw_input('Enter the "Boursorama Banque" access code:')
#print "Pin access code: ''%s''" % (pin)
url = "https://" + DOMAIN + "/ajax/banque/otp.phtml"
data = "authentificationforteToken=%s&authentificationforteStep=otp&alertType=10100&org=%s&otp=%s&validate=" % (token, REFERER, pin)
req = urllib2.Request(url, data, headers_ajax)
response = self.browser.open(req)
#info = response.read()
#print "after pin authentification", len(info), response.info()
#write_debug(info, "step3.html")
#self.print_cookies()
url = "%s?" % (SECURE_PAGE)
data = "org=/&device=%s" % (device)
req = urllib2.Request(url, data, headers=headers)
response = self.browser.open(req)
#result = response.read()
#print response, "\n", response.info()
#write_debug(result, "step4.html")
#self.print_cookies()
def print_cookies(self):
for c in self.browser._ua_handlers["_cookies"].cookiejar:
print "%s : %s" % (c.name, c.value)