pax_global_header 0000666 0000000 0000000 00000000064 14575653726 0014536 g ustar 00root root 0000000 0000000 52 comment=5f3d558793b537a74480241ac6981479f5938cd3
woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/ 0000775 0000000 0000000 00000000000 14575653726 0024652 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/ 0000775 0000000 0000000 00000000000 14575653726 0026322 5 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/franceconnect/ 0000775 0000000 0000000 00000000000 14575653726 0031132 5 ustar 00root root 0000000 0000000 __init__.py 0000664 0000000 0000000 00000001506 14575653726 0033166 0 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/franceconnect # Copyright(C) 2012-2020 Powens
#
# This file is part of a woob module.
#
# This woob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This woob module 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
# flake8: compatible
from .module import FranceConnectModule
__all__ = ['FranceConnectModule']
browser.py 0000664 0000000 0000000 00000012126 14575653726 0033112 0 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/franceconnect # Copyright(C) 2012-2020 Powens
#
# This file is part of a woob module.
#
# This woob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This woob module 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
# flake8: compatible
from urllib.parse import urlparse
from woob.browser import LoginBrowser, URL
from woob.exceptions import BrowserIncorrectPassword, BrowserUserBanned
from .pages import (
AuthorizePage, AmeliLoginPage, WrongPassAmeliLoginPage, ImpotsLoginAccessPage,
ImpotsLoginAELPage, ImpotsGetContextPage,
)
class FranceConnectBrowser(LoginBrowser):
"""
france connect urls work only with nss
"""
BASEURL = 'https://app.franceconnect.gouv.fr'
# re set BASEURL to authorize page,
# because it has to be always same BASEURL, no matter which child module use it with his own BASEURL
authorize = URL(r'https://app.franceconnect.gouv.fr/api/v1/authorize', AuthorizePage)
ameli_login_page = URL(r'/FRCO-app/login', AmeliLoginPage)
ameli_wrong_login_page = URL(r'/FRCO-app/j_spring_security_check', WrongPassAmeliLoginPage)
impot_login_page = URL(r'https://idp.impots.gouv.fr/LoginAccess', ImpotsLoginAccessPage)
impot_login_ael = URL(r'https://idp.impots.gouv.fr/LoginAEL', ImpotsLoginAELPage)
impot_get_context = URL(r'https://idp.impots.gouv.fr/GetContexte', ImpotsGetContextPage)
def fc_call(self, provider, baseurl):
self.BASEURL = baseurl
params = {'provider': provider, 'storeFI': 'false'}
self.location('/call', params=params)
def fc_redirect(self, url=None):
self.BASEURL = 'https://app.franceconnect.gouv.fr'
if url is not None:
self.location(url)
error_message = self.page.get_error_message()
if error_message:
if error_message == 'Les identifiants utilisés correspondent à une identité qui ne permet plus la connexion via FranceConnect.':
raise BrowserUserBanned(error_message)
raise AssertionError(error_message)
self.page.redirect()
parse_result = urlparse(self.url)
self.BASEURL = parse_result.scheme + '://' + parse_result.netloc
def login_impots(self, fc_redirection=True):
"""
Login using the service impots.gouv.fr
:param fc_redirection: whether or not to redirect to and out of the
specific service
"""
if fc_redirection:
self.fc_call('dgfip', 'https://idp.impots.gouv.fr')
context_url = self.page.get_url_context()
url_login_password = self.page.get_url_login_password()
# POST /GetContexte (ImpotsGetContextPage)
context_page = self.open(context_url, data={"spi": self.username}).page
if context_page.has_wrong_login():
raise BrowserIncorrectPassword(bad_fields=['login'])
if context_page.is_blocked():
raise BrowserUserBanned(
"Pour votre sécurité, l'accès à votre espace a été bloqué ; veuillez contacter votre centre des Finances publiques."
)
assert context_page.has_next_step(), 'Unexpected behaviour after submitting login for France Connect impôts'
# POST /LoginAEL (ImpotsLoginAELPage)
self.page.login(self.username, self.password, url_login_password)
if self.page.has_wrong_password():
remaining_attemps = self.page.get_remaining_login_attempts()
attemps_str = f'{remaining_attemps} essai'
if int(remaining_attemps) > 1:
attemps_str = f'{remaining_attemps} essais'
message = f'Votre mot de passe est incorrect, il vous reste {attemps_str} pour vous identifier.'
raise BrowserIncorrectPassword(message, bad_fields=['password'])
assert self.page.is_status_ok(), 'Unexpected behaviour after submitting password for France Connect impôts'
next_url = self.page.get_next_url()
self.location(next_url)
if fc_redirection:
self.fc_redirect()
def login_ameli(self, fc_redirection=True):
"""
Login using the service ameli.fr
:param fc_redirection: whether or not to redirect to and out of the
specific service
"""
if fc_redirection:
self.fc_call('ameli', 'https://fc.assure.ameli.fr')
self.page.login(self.username, self.password)
if self.ameli_wrong_login_page.is_here():
msg = self.page.get_error_message()
if msg:
raise BrowserIncorrectPassword(msg)
raise AssertionError('Unexpected behaviour at login')
if fc_redirection:
self.fc_redirect()
module.py 0000664 0000000 0000000 00000002270 14575653726 0032713 0 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/franceconnect # Copyright(C) 2012-2020 Powens
#
# This file is part of a woob module.
#
# This woob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This woob module 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
# flake8: compatible
from woob.tools.backend import Module
from woob.capabilities.bill import CapDocument
from .browser import FranceConnectBrowser
__all__ = ['FranceConnectModule']
class FranceConnectModule(Module, CapDocument):
NAME = 'franceconnect'
DESCRIPTION = 'France Connect website'
MAINTAINER = 'Florian Duguet'
EMAIL = 'florian.duguet@budget-insight.com'
LICENSE = 'LGPLv3+'
VERSION = '3.6'
BROWSER = FranceConnectBrowser
pages.py 0000664 0000000 0000000 00000010262 14575653726 0032525 0 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/franceconnect # Copyright(C) 2012-2020 Powens
#
# This file is part of a woob module.
#
# This woob module is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This woob module 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this woob module. If not, see .
# flake8: compatible
import re
from woob.browser.filters.javascript import JSValue, JSVar
from woob.browser.filters.standard import (
CleanText,
)
from woob.browser.pages import HTMLPage
class AuthorizePage(HTMLPage):
def get_error_message(self):
return CleanText('//h1[@data-testid=error-section-title]')(self.doc)
def redirect(self):
# just one form on this page, so get_form() should work but it's better to put a more restrictive xpath
form = self.get_form(xpath='//form[@action="/confirm-redirect-client"]')
form.submit()
class AmeliLoginPage(HTMLPage):
def login(self, username, password):
# CAUTION, form id, username and password keys are not the same than the one of standard ameli login page
form = self.get_form(id='connexion_form')
form['j_username'] = username
form['j_password'] = password
form.submit()
class WrongPassAmeliLoginPage(HTMLPage):
def get_error_message(self):
return CleanText('//div[@id="divErreur"]')(self.doc)
class ImpotsLoginAccessPage(HTMLPage):
def login(self, login, password, url):
form = self.get_form(id='formulairePrincipal')
form.url = url
form['spi'] = login
form['pwd'] = password
form.submit()
def get_url_context(self):
return JSVar(
CleanText("//script[contains(text(), 'urlContexte')]"),
var="urlContexte",
)(self.doc)
def get_url_login_password(self):
return JSVar(
CleanText("//script[contains(text(), 'urlLoginMotDePasse')]"),
var="urlLoginMotDePasse",
)(self.doc)
class MessageResultPage(HTMLPage):
status = None
message = None
def load_status_and_message_from_post_message(self):
if not self.status or not self.message:
# parent.postMessage(args...)
first_argument = JSValue(CleanText('//script[contains(text(), "parent.postMessage")]'), nth=0)(self.doc)
# The message is separated in 2 parts with a comma
message_parts = first_argument.split(",")
assert len(message_parts) == 2, 'Unexpected message from France Connect impôts'
self.status, self.message = message_parts
class ImpotsGetContextPage(MessageResultPage):
def has_wrong_login(self):
self.load_status_and_message_from_post_message()
return self.message == 'EXISTEPAS'
def is_blocked(self):
self.load_status_and_message_from_post_message()
return self.message == 'BLOCAGE'
def has_next_step(self):
self.load_status_and_message_from_post_message()
return self.status == 'ctx' and self.message == 'LMDP'
class ImpotsLoginAELPage(MessageResultPage):
def get_next_url(self):
self.load_status_and_message_from_post_message()
assert re.match(r'^https?://.*$', self.message), f'Unexpected message: {self.message}'
return self.message
def has_wrong_password(self):
self.load_status_and_message_from_post_message()
# 4005 is the code for a wrong password followed by the number of remaining
# attempts
return self.status == 'lmdp' and re.match(r'^4005:\d+$', self.message)
def get_remaining_login_attempts(self):
self.load_status_and_message_from_post_message()
return re.match(r'^4005:(\d+)$', self.message).group(1)
def is_status_ok(self):
self.load_status_and_message_from_post_message()
return self.status == 'ok'
requirements.txt 0000664 0000000 0000000 00000000014 14575653726 0034332 0 ustar 00root root 0000000 0000000 woob-master-5f3d558793b537a74480241ac6981479f5938cd3-modules-franceconnect/modules/franceconnect woob ~= 3.2