From 25338c6c61da849cedaec4fd15be61e169a1cc48 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Tue, 8 Oct 2024 11:05:30 +0200 Subject: [PATCH] pro_connect: Add a new SSO --- CHANGELOG_breaking_changes.md | 6 + config/settings/base.py | 6 + config/urls.py | 2 + itou/openid_connect/pro_connect/__init__.py | 0 itou/openid_connect/pro_connect/apps.py | 6 + itou/openid_connect/pro_connect/constants.py | 21 + itou/openid_connect/pro_connect/enums.py | 10 + .../pro_connect/migrations/0001_initial.py | 31 + .../pro_connect/migrations/__init__.py | 0 itou/openid_connect/pro_connect/models.py | 63 ++ itou/openid_connect/pro_connect/urls.py | 13 + itou/openid_connect/pro_connect/views.py | 350 +++++++ itou/static/css/itou.css | 16 + itou/static/img/illustration-pc.svg | 27 + itou/static/img/pro_connect_bouton.svg | 1 + itou/static/img/pro_connect_bouton_hover.svg | 1 + .../activate_inclusion_connect_account.html | 75 +- itou/templates/account/login_generic.html | 16 +- itou/templates/dashboard/edit_user_info.html | 29 +- .../includes/description.html | 25 +- itou/users/adapter.py | 9 + itou/users/enums.py | 1 + itou/users/migrations/0001_initial.py | 1 + itou/utils/perms/middleware.py | 3 +- itou/www/dashboard/views.py | 11 +- itou/www/invitations_views/views.py | 10 +- itou/www/login/views.py | 24 +- itou/www/signup/views.py | 30 +- .../openid_connect/inclusion_connect/test.py | 127 ++- .../openid_connect/inclusion_connect/tests.py | 478 ++++----- tests/openid_connect/pro_connect/__init__.py | 0 tests/openid_connect/pro_connect/test.py | 136 +++ tests/openid_connect/pro_connect/tests.py | 973 ++++++++++++++++++ tests/openid_connect/test.py | 20 + ...ccount.py => test_activate_sso_account.py} | 37 +- tests/www/dashboard/test_edit_user_info.py | 54 +- .../invitations_views/test_company_accept.py | 207 ++-- .../test_prescriber_organization.py | 116 +-- tests/www/login/tests.py | 100 +- tests/www/signup/test_prescriber.py | 562 +++++----- tests/www/signup/test_siae.py | 263 +++-- tests/www/welcoming_tour/tests.py | 17 +- 42 files changed, 2911 insertions(+), 966 deletions(-) create mode 100644 itou/openid_connect/pro_connect/__init__.py create mode 100644 itou/openid_connect/pro_connect/apps.py create mode 100644 itou/openid_connect/pro_connect/constants.py create mode 100644 itou/openid_connect/pro_connect/enums.py create mode 100644 itou/openid_connect/pro_connect/migrations/0001_initial.py create mode 100644 itou/openid_connect/pro_connect/migrations/__init__.py create mode 100644 itou/openid_connect/pro_connect/models.py create mode 100644 itou/openid_connect/pro_connect/urls.py create mode 100644 itou/openid_connect/pro_connect/views.py create mode 100644 itou/static/img/illustration-pc.svg create mode 100644 itou/static/img/pro_connect_bouton.svg create mode 100644 itou/static/img/pro_connect_bouton_hover.svg create mode 100644 tests/openid_connect/pro_connect/__init__.py create mode 100644 tests/openid_connect/pro_connect/test.py create mode 100644 tests/openid_connect/pro_connect/tests.py create mode 100644 tests/openid_connect/test.py rename tests/www/dashboard/{test_activate_ic_account.py => test_activate_sso_account.py} (68%) diff --git a/CHANGELOG_breaking_changes.md b/CHANGELOG_breaking_changes.md index 226e296140..5dc90f5d20 100644 --- a/CHANGELOG_breaking_changes.md +++ b/CHANGELOG_breaking_changes.md @@ -1,4 +1,10 @@ # Journal des changements techniques majeurs +## 2024-09-23 + +- Ajout des variables d'environnement `PRO_CONNECT_*` + la recette ProConnect peut être utilisée en créant une recette jetable + et en suivant les indications de la note bitwarden + ## 2024-09-22 - Ajout de la variable d'environnement `API_PARTICULIER_TOKEN` pour appeler l'API en local. diff --git a/config/settings/base.py b/config/settings/base.py index 582a8f4613..e72c363074 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -84,6 +84,7 @@ "itou.openid_connect.france_connect", "itou.openid_connect.inclusion_connect", "itou.openid_connect.pe_connect", + "itou.openid_connect.pro_connect", "itou.invitations", "itou.external_data", "itou.metabase", @@ -417,6 +418,11 @@ INCLUSION_CONNECT_CLIENT_ID = os.getenv("INCLUSION_CONNECT_CLIENT_ID") INCLUSION_CONNECT_CLIENT_SECRET = os.getenv("INCLUSION_CONNECT_CLIENT_SECRET") +PRO_CONNECT_BASE_URL = os.getenv("PRO_CONNECT_BASE_URL") +PRO_CONNECT_CLIENT_ID = os.getenv("PRO_CONNECT_CLIENT_ID") +PRO_CONNECT_CLIENT_SECRET = os.getenv("PRO_CONNECT_CLIENT_SECRET") +PRO_CONNECT_FT_IDP_HINT = os.getenv("PRO_CONNECT_FT_IDP_HINT") + TALLY_URL = os.getenv("TALLY_URL") METABASE_HOST = os.getenv("METABASE_HOST") diff --git a/config/urls.py b/config/urls.py index 80b4402869..402026aac2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -48,6 +48,8 @@ path("franceconnect/", include("itou.openid_connect.france_connect.urls")), # Inclusion Connect URLs. path("inclusion_connect/", include("itou.openid_connect.inclusion_connect.urls")), + # ProConnect URLs. + path("pro_connect/", include("itou.openid_connect.pro_connect.urls")), # -------------------------------------------------------------------------------------- # API. path("api/v1/", include("itou.api.urls", namespace="v1")), diff --git a/itou/openid_connect/pro_connect/__init__.py b/itou/openid_connect/pro_connect/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/itou/openid_connect/pro_connect/apps.py b/itou/openid_connect/pro_connect/apps.py new file mode 100644 index 0000000000..28aae3dd1a --- /dev/null +++ b/itou/openid_connect/pro_connect/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProConnectConfig(AppConfig): + name = "itou.openid_connect.pro_connect" + verbose_name = "ProConnect" diff --git a/itou/openid_connect/pro_connect/constants.py b/itou/openid_connect/pro_connect/constants.py new file mode 100644 index 0000000000..a1debea19b --- /dev/null +++ b/itou/openid_connect/pro_connect/constants.py @@ -0,0 +1,21 @@ +from django.conf import settings + + +# https://github.com/numerique-gouv/agentconnect-documentation/blob/main/doc_fs/donnees_fournies.md +# We should not need to add the email, given_name and usual_name but it doesn"t work without them... +PRO_CONNECT_SCOPES = "openid email given_name usual_name custom" + +PRO_CONNECT_CLIENT_ID = settings.PRO_CONNECT_CLIENT_ID +PRO_CONNECT_CLIENT_SECRET = settings.PRO_CONNECT_CLIENT_SECRET + +PRO_CONNECT_ENDPOINT_AUTHORIZE = f"{settings.PRO_CONNECT_BASE_URL}/authorize" +PRO_CONNECT_ENDPOINT_TOKEN = f"{settings.PRO_CONNECT_BASE_URL}/token" +PRO_CONNECT_ENDPOINT_USERINFO = f"{settings.PRO_CONNECT_BASE_URL}/userinfo" +PRO_CONNECT_ENDPOINT_LOGOUT = f"{settings.PRO_CONNECT_BASE_URL}/session/end" + +# This timeout (in seconds) has been chosen arbitrarily. +PRO_CONNECT_TIMEOUT = 60 + +PRO_CONNECT_SESSION_KEY = "pro_connect" + +PRO_CONNECT_FT_IDP_HINT = settings.PRO_CONNECT_FT_IDP_HINT diff --git a/itou/openid_connect/pro_connect/enums.py b/itou/openid_connect/pro_connect/enums.py new file mode 100644 index 0000000000..79ee0a8e3e --- /dev/null +++ b/itou/openid_connect/pro_connect/enums.py @@ -0,0 +1,10 @@ +import enum + + +class ProConnectChannel(str, enum.Enum): + """This enum is stored in the session, and allow us to change the error message + in the callback view depending on where the user came from. + """ + + INVITATION = "invitation" + MAP_CONSEILLER = "map_conseiller" diff --git a/itou/openid_connect/pro_connect/migrations/0001_initial.py b/itou/openid_connect/pro_connect/migrations/0001_initial.py new file mode 100644 index 0000000000..ba0d61d316 --- /dev/null +++ b/itou/openid_connect/pro_connect/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 5.0.3 on 2024-03-22 09:37 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ProConnectState", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "created_at", + models.DateTimeField( + db_index=True, default=django.utils.timezone.now, verbose_name="date de création" + ), + ), + ("used_at", models.DateTimeField(null=True, verbose_name="date d'utilisation")), + ("data", models.JSONField(blank=True, default=dict, verbose_name="données de session")), + ("state", models.CharField(max_length=12, unique=True)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/itou/openid_connect/pro_connect/migrations/__init__.py b/itou/openid_connect/pro_connect/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/itou/openid_connect/pro_connect/models.py b/itou/openid_connect/pro_connect/models.py new file mode 100644 index 0000000000..5a063d80f3 --- /dev/null +++ b/itou/openid_connect/pro_connect/models.py @@ -0,0 +1,63 @@ +import dataclasses +import logging +from typing import ClassVar + +from django.db import models + +from itou.prescribers.models import PrescriberOrganization +from itou.users.enums import IdentityProvider, UserKind +from itou.users.models import User + +from ..models import OIDConnectState, OIDConnectUserData + + +logger = logging.getLogger(__name__) + + +class ProConnectState(OIDConnectState): + data = models.JSONField(verbose_name="données de session", default=dict, blank=True) + + +@dataclasses.dataclass +class ProConnectUserData(OIDConnectUserData): + @staticmethod + def user_info_mapping_dict(user_info: dict): + return { + "username": user_info["sub"], + "first_name": user_info["given_name"], + "last_name": user_info["usual_name"], + "email": user_info["email"], + } + + def join_org(self, user: User, safir: str): + if not user.is_prescriber: + raise ValueError("Invalid user kind: %s", user.kind) + try: + organization = PrescriberOrganization.objects.get(code_safir_pole_emploi=safir) + except PrescriberOrganization.DoesNotExist: + logger.error(f"Organization with SAFIR {safir} does not exist. Unable to add user {user.email}.") + raise + if not organization.has_member(user): + organization.add_or_activate_member(user) + + +@dataclasses.dataclass +class ProConnectPrescriberData(ProConnectUserData): + kind: UserKind = UserKind.PRESCRIBER + identity_provider: IdentityProvider = IdentityProvider.PRO_CONNECT + login_allowed_user_kinds: ClassVar[tuple[UserKind]] = (UserKind.PRESCRIBER, UserKind.EMPLOYER) + allowed_identity_provider_migration: ClassVar[tuple[IdentityProvider]] = ( + IdentityProvider.DJANGO, + IdentityProvider.INCLUSION_CONNECT, + ) + + +@dataclasses.dataclass +class ProConnectEmployerData(ProConnectUserData): + kind: UserKind = UserKind.EMPLOYER + identity_provider: IdentityProvider = IdentityProvider.PRO_CONNECT + login_allowed_user_kinds: ClassVar[tuple[UserKind]] = (UserKind.PRESCRIBER, UserKind.EMPLOYER) + allowed_identity_provider_migration: ClassVar[tuple[IdentityProvider]] = ( + IdentityProvider.DJANGO, + IdentityProvider.INCLUSION_CONNECT, + ) diff --git a/itou/openid_connect/pro_connect/urls.py b/itou/openid_connect/pro_connect/urls.py new file mode 100644 index 0000000000..e93b0cf770 --- /dev/null +++ b/itou/openid_connect/pro_connect/urls.py @@ -0,0 +1,13 @@ +from django.urls import path + +from . import views + + +app_name = "pro_connect" + +urlpatterns = [ + path("authorize", views.pro_connect_authorize, name="authorize"), + path("callback", views.pro_connect_callback, name="callback"), + path("logout", views.pro_connect_logout, name="logout"), + path("logout_callback", views.pro_connect_logout_callback, name="logout_callback"), +] diff --git a/itou/openid_connect/pro_connect/views.py b/itou/openid_connect/pro_connect/views.py new file mode 100644 index 0000000000..2c805d4e94 --- /dev/null +++ b/itou/openid_connect/pro_connect/views.py @@ -0,0 +1,350 @@ +import dataclasses +import logging + +import httpx +import jwt +from allauth.account.adapter import get_adapter +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import login +from django.http import HttpResponseRedirect +from django.urls import reverse +from django.utils import crypto +from django.utils.html import format_html +from django.utils.http import url_has_allowed_host_and_scheme, urlencode + +from itou.prescribers.models import PrescriberOrganization +from itou.users.enums import KIND_EMPLOYER, KIND_PRESCRIBER, UserKind +from itou.users.models import User +from itou.utils import constants as global_constants +from itou.utils.constants import ITOU_HELP_CENTER_URL +from itou.utils.urls import add_url_params, get_absolute_url + +from ..models import EmailInUseException, InvalidKindException, MultipleUsersFoundException +from . import constants +from .enums import ProConnectChannel +from .models import ( + ProConnectEmployerData, + ProConnectPrescriberData, + ProConnectState, +) + + +logger = logging.getLogger(__name__) + +USER_DATA_CLASSES = { + KIND_PRESCRIBER: ProConnectPrescriberData, + KIND_EMPLOYER: ProConnectEmployerData, +} + + +@dataclasses.dataclass +class ProConnectStateData: + previous_url: str = None + next_url: str = None + user_email: str = None + user_kind: str = None + channel: str = None + # Tells us where did the user came from so that we can adapt + # error messages in the callback view. + is_login: bool = False # Used to skip kind check and allow login through the wrong user kind + prescriber_session_data: dict = None + + +@dataclasses.dataclass +class ProConnectSession: + key: str = constants.PRO_CONNECT_SESSION_KEY + token: str = None + state: str = None + + def bind_to_request(self, request): + request.session[self.key] = dataclasses.asdict(self) + + +def _redirect_to_login_page_on_error(error_msg=None, request=None): + if request: + messages.error(request, "Une erreur technique est survenue. Merci de recommencer.") + if error_msg: + logger.error(error_msg, exc_info=True) + return HttpResponseRedirect(reverse("search:employers_home")) + + +def _generate_pro_params_from_session(pc_data): + redirect_uri = get_absolute_url(reverse("pro_connect:callback")) + state = ProConnectState.save_state(data=pc_data) + data = { + "response_type": "code", + "client_id": constants.PRO_CONNECT_CLIENT_ID, + "redirect_uri": redirect_uri, + "acr_values": "eidas1", + "scope": constants.PRO_CONNECT_SCOPES, + "state": state, + "nonce": crypto.get_random_string(length=12), + } + if pc_data.get("channel") == ProConnectChannel.MAP_CONSEILLER: + data["idp_hint"] = constants.PRO_CONNECT_FT_IDP_HINT + if user_email := pc_data.get("user_email"): + data["login_hint"] = user_email + return data + + +def _add_user_kind_error_message(request, existing_user, new_user_kind): + messages.error( + request, + format_html( + "Un compte {} existe déjà avec cette adresse e-mail. " + "Vous devez créer un compte ProConnect avec une autre adresse e-mail pour " + "devenir {} sur la plateforme. Besoin d'aide ? " + "Contactez-nous.", + existing_user.get_kind_display(), + UserKind(new_user_kind).label, + ITOU_HELP_CENTER_URL, + ), + ) + + +def pro_connect_authorize(request): + # Start a new session. + user_kind = request.GET.get("user_kind") + previous_url = request.GET.get("previous_url", reverse("search:employers_home")) + next_url = request.GET.get("next_url") + if next_url and not url_has_allowed_host_and_scheme(next_url, settings.ALLOWED_HOSTS, request.is_secure()): + return _redirect_to_login_page_on_error(error_msg="Forbidden external url") + register = request.GET.get("register") + if not user_kind: + return _redirect_to_login_page_on_error(error_msg="User kind missing.") + if user_kind not in USER_DATA_CLASSES: + return _redirect_to_login_page_on_error(error_msg="Wrong user kind.") + + pc_data = ProConnectStateData( + user_kind=user_kind, previous_url=previous_url, next_url=next_url, is_login=not register + ) + + if channel := request.GET.get("channel"): + pc_data.channel = channel + + if user_email := request.GET.get("user_email"): + pc_data.user_email = user_email + + if session_data := request.session.get(global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY): + pc_data.prescriber_session_data = {global_constants.ITOU_SESSION_PRESCRIBER_SIGNUP_KEY: session_data} + + data = _generate_pro_params_from_session(dataclasses.asdict(pc_data)) + # Store the state in session to allow the user to use resume registration view + pc_session = ProConnectSession(state=data["state"]) + pc_session.bind_to_request(request) + + base_url = constants.PRO_CONNECT_ENDPOINT_AUTHORIZE + return HttpResponseRedirect(f"{base_url}?{urlencode(data)}") + + +def _get_token(request, code): + # Retrieve token from ProConnect + token_redirect_uri = get_absolute_url(reverse("pro_connect:callback")) + data = { + "client_id": constants.PRO_CONNECT_CLIENT_ID, + "client_secret": constants.PRO_CONNECT_CLIENT_SECRET, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": token_redirect_uri, + } + response = httpx.post( + constants.PRO_CONNECT_ENDPOINT_TOKEN, + data=data, + timeout=constants.PRO_CONNECT_TIMEOUT, + ) + # Contains access_token, token_type, expires_in, id_token + if response.status_code != 200: + return None, _redirect_to_login_page_on_error(error_msg="Impossible to get token.", request=request) + return response.json(), None + + +def _get_user_info(request, access_token): + response = httpx.get( + constants.PRO_CONNECT_ENDPOINT_USERINFO, + params={"schema": "openid"}, + headers={"Authorization": "Bearer " + access_token}, + timeout=constants.PRO_CONNECT_TIMEOUT, + ) + if response.status_code != 200: + return None, _redirect_to_login_page_on_error(error_msg="Impossible to get user infos.", request=request) + decoded_id_token = jwt.decode( + response.content, + key=constants.PRO_CONNECT_CLIENT_SECRET, + algorithms=["HS256"], + audience=constants.PRO_CONNECT_CLIENT_ID, + ) + return decoded_id_token, None + + +def pro_connect_callback(request): + code = request.GET.get("code") + state = request.GET.get("state") + if code is None or state is None: + return _redirect_to_login_page_on_error(error_msg="Missing code or state.", request=request) + + # Get access token now to have more data in sentry + token_data, error_redirection = _get_token(request, code) + if error_redirection: + return error_redirection + access_token = token_data.get("access_token") + if not access_token: + return _redirect_to_login_page_on_error(error_msg="Access token field missing.", request=request) + + # Check if state is valid and session exists + pro_connect_state = ProConnectState.get_from_state(state) + if pro_connect_state is None or not pro_connect_state.is_valid(): + return _redirect_to_login_page_on_error(request=request) + pc_session = ProConnectSession(state=state, token=token_data["id_token"]) + pc_session.bind_to_request(request) + + # A token has been provided so it's time to fetch associated user infos + # because the token is only valid for 5 seconds. + # We don't really need to access user_info since all we need is already in the access_token. + # Should we remove this ? + user_data, error_redirection = _get_user_info(request, access_token) + if error_redirection: + return error_redirection + + if "sub" not in user_data: + # 'sub' is the unique identifier from ProConnect, we need that to match a user later on. + return _redirect_to_login_page_on_error(error_msg="Sub parameter error.", request=request) + + user_kind = pro_connect_state.data["user_kind"] + is_successful = True + pc_user_data = USER_DATA_CLASSES[user_kind].from_user_info(user_data) + pc_session_email = pro_connect_state.data.get("user_email") + + if pc_session_email and pc_session_email.lower() != pc_user_data.email.lower(): + if pro_connect_state.data["channel"] == ProConnectChannel.INVITATION: + error = ( + "L’adresse e-mail que vous avez utilisée pour vous connecter avec ProConnect " + f"({pc_user_data.email}) " + f"ne correspond pas à l’adresse e-mail de l’invitation ({pc_session_email})." + ) + else: + error = ( + "L’adresse e-mail que vous avez utilisée pour vous connecter avec ProConnect " + f"({pc_user_data.email}) " + f"est différente de celle que vous avez indiquée précédemment ({pc_session_email})." + ) + messages.error(request, error) + is_successful = False + + try: + user, _ = pc_user_data.create_or_update_user(is_login=pro_connect_state.data.get("is_login")) + except InvalidKindException: + existing_user = User.objects.get(email=user_data["email"]) + _add_user_kind_error_message(request, existing_user, user_kind) + is_successful = False + except EmailInUseException as e: + redacted_name = e.user.get_redacted_full_name() + msg_who = ( + format_html( + " au nom de {}", + redacted_name, + ) + if redacted_name + else "" + ) + + error = format_html( + "Vous avez essayé de vous connecter avec un compte ProConnect, mais un compte" + "{} a déjà été créé avec cette adresse e-mail. " + "Veuillez vous rapprocher du support pour débloquer la situation en suivant " + "ce lien.", + msg_who, + global_constants.ITOU_HELP_CENTER_URL, + ) + messages.error(request, error) + is_successful = False + except MultipleUsersFoundException as e: + # Here we have a user trying to update his email, but with an already existing email + # let him login, but display a message because we didn't update his email + messages.error( + request, + format_html( + "L'adresse e-mail que nous a transmis ProConnect est différente de celle qui est enregistrée " + "sur la plateforme des emplois, et est déjà associé à un autre compte. " + "Nous n'avons donc pas pu mettre à jour {} en {}. " + "Veuillez vous rapprocher du support pour débloquer la situation en suivant " + "ce lien.", + e.users[0].email, + e.users[1].email, + global_constants.ITOU_HELP_CENTER_URL, + ), + ) + user = e.users[0] + + code_safir_pole_emploi = user_data.get("custom", {}).get("structureTravail") + # Only handle user creation for the moment, not updates. + if is_successful and user.is_prescriber and code_safir_pole_emploi: + try: + pc_user_data.join_org(user=user, safir=code_safir_pole_emploi) + except PrescriberOrganization.DoesNotExist: + messages.warning( + request, + format_html( + "L'agence indiquée par NEPTUNE (code SAFIR {}) n'est pas référencée dans notre service. " + "Cela arrive quand vous appartenez à un Point Relais mais que vous êtes rattaché à une agence " + "mère sur la plateforme des emplois. " + "Si vous pensez qu'il y a une erreur, vérifiez que le code SAFIR est le bon " + "puis contactez le support en indiquant le code SAFIR.", + code_safir_pole_emploi, + global_constants.ITOU_HELP_CENTER_URL, + ), + ) + + if not is_successful: + logout_url_params = { + "redirect_url": pro_connect_state.data["previous_url"], + } + next_url = f"{reverse('pro_connect:logout')}?{urlencode(logout_url_params)}" + return HttpResponseRedirect(next_url) + + # Because we have more than one Authentication backend in our settings, we need to specify + # the one we want to use in login + login(request, user, backend="django.contrib.auth.backends.ModelBackend") + + # reattach prescriber session + if prescriber_session_data := pro_connect_state.data.get("prescriber_session_data"): + request.session.update(prescriber_session_data) + + next_url = pro_connect_state.data["next_url"] or get_adapter(request).get_login_redirect_url(request) + return HttpResponseRedirect(next_url) + + +def pro_connect_logout(request): + token = request.GET.get("token") + post_logout_redirect_url = reverse("pro_connect:logout_callback") + redirect_url = request.GET.get("redirect_url", reverse("search:employers_home")) + + # Fallback on session data. + if not token: + pc_session = request.session.get(constants.PRO_CONNECT_SESSION_KEY) + if not pc_session: + raise KeyError("Missing session key.") + token = pc_session["token"] + + logout_state = ProConnectState.save_state(data={"redirect_url": redirect_url}) + + params = { + "id_token_hint": token, + "state": logout_state, + "post_logout_redirect_uri": get_absolute_url(post_logout_redirect_url), + } + complete_url = add_url_params(constants.PRO_CONNECT_ENDPOINT_LOGOUT, params) + return HttpResponseRedirect(complete_url) + + +def pro_connect_logout_callback(request): + state = request.GET.get("state") + if state is None: + return _redirect_to_login_page_on_error(error_msg="Missing state.", request=request) + + # Check if state is valid and session exists + pro_connect_state = ProConnectState.get_from_state(state) + if pro_connect_state is None or not pro_connect_state.is_valid(): + return _redirect_to_login_page_on_error(request=request) + + return HttpResponseRedirect(pro_connect_state.data["redirect_url"]) diff --git a/itou/static/css/itou.css b/itou/static/css/itou.css index 15da34e05f..b47e78186b 100644 --- a/itou/static/css/itou.css +++ b/itou/static/css/itou.css @@ -416,3 +416,19 @@ an input field being invalid, generating an uncontrolled red box-shadow. */ .w-lg-400px .select2-selection__rendered { white-space: nowrap !important; } + + +/* Pro Connect button theme */ +.proconnect-button { + background-color: transparent !important; + background-image: url("../img/pro_connect_bouton.svg"); + background-position: 50% 50%; + background-repeat: no-repeat; + width: 214px; + height: 56px; + display: inline-block; +} + +.proconnect-button:hover { + background-image: url("../img/pro_connect_bouton_hover.svg"); +} diff --git a/itou/static/img/illustration-pc.svg b/itou/static/img/illustration-pc.svg new file mode 100644 index 0000000000..2ad51be694 --- /dev/null +++ b/itou/static/img/illustration-pc.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/itou/static/img/pro_connect_bouton.svg b/itou/static/img/pro_connect_bouton.svg new file mode 100644 index 0000000000..289890596c --- /dev/null +++ b/itou/static/img/pro_connect_bouton.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/itou/static/img/pro_connect_bouton_hover.svg b/itou/static/img/pro_connect_bouton_hover.svg new file mode 100644 index 0000000000..fcdd7df7ff --- /dev/null +++ b/itou/static/img/pro_connect_bouton_hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/itou/templates/account/activate_inclusion_connect_account.html b/itou/templates/account/activate_inclusion_connect_account.html index 63ce6bbcd3..c94de0e6fb 100644 --- a/itou/templates/account/activate_inclusion_connect_account.html +++ b/itou/templates/account/activate_inclusion_connect_account.html @@ -8,38 +8,63 @@ {% block title %}Connexion {{ block.super }}{% endblock %} -{% block body_class %}p-inclusion-connect{% endblock %} - {% block content %}
-
- - Inclusion Connect - -

Un accès unique à tous vos services !

-

Le service Inclusion Connect est à présent obligatoire pour se connecter à la plateforme des emplois.

-

- Il vous permettra d'accéder aux différents services partenaires avec le même identifiant et le même mot de passe. -

-

Il vous suffit de cliquer sur le bouton ci-desous pour activer votre compte

- - Activer mon compte Inclusion Connect - - - Plus d'infos - - -
+ {% if pro_connect_url %} +
+

ProConnect, un accès unique à tous vos services !

+

Le service ProConnect est obligatoire pour se connecter au service les emplois de l'inclusion.

+

+ Pour conserver l'accès à votre compte existant vous devez vous connecter ou créer un compte avec ProConnect à l'aide du bouton ci-dessous. +

+

+ Assurez vous que l'adresse e-mail utilisée soit la suivante : +
+ {{ user.email }} +

+
+ + +
+ + Plus d'infos + + +
+ {% else %} +
+ + Inclusion Connect + +

Un accès unique à tous vos services !

+

Le service Inclusion Connect est à présent obligatoire pour se connecter à la plateforme des emplois.

+

+ Il vous permettra d'accéder aux différents services partenaires avec le même identifiant et le même mot de passe. +

+

Il vous suffit de cliquer sur le bouton ci-desous pour activer votre compte

+ + Activer mon compte Inclusion Connect + + + Plus d'infos + + +
+ {% endif %}
- + {% if pro_connect_url %} + + {% else %} + + {% endif %}
diff --git a/itou/templates/account/login_generic.html b/itou/templates/account/login_generic.html index f0de73f694..e7f4d20d68 100644 --- a/itou/templates/account/login_generic.html +++ b/itou/templates/account/login_generic.html @@ -14,9 +14,23 @@
+
+

Inclusion Connect devient ProConnect

+
{% if uses_inclusion_connect %} - {% if inclusion_connect_url %} + {% if pro_connect_url %} + + + {% elif inclusion_connect_url %}
diff --git a/itou/templates/dashboard/edit_user_info.html b/itou/templates/dashboard/edit_user_info.html index 25bfa2028b..3266f3e659 100644 --- a/itou/templates/dashboard/edit_user_info.html +++ b/itou/templates/dashboard/edit_user_info.html @@ -55,7 +55,34 @@

Informations personnelles

{% else %}
{% csrf_token %} - {% if ic_account_url %} + {% if request.user.identity_provider == "PC" %} +
    +
  • + Prénom : {{ user.first_name|title }} +
  • +
  • + Nom : {{ user.last_name|upper }} +
  • +
  • + Adresse e-mail : {{ user.email }} +
  • +
+
+

+ Ces informations doivent être modifiées sur votre fournisseur d'identité. +
+ - Si votre compte a été créé sur ProConnect, vous pouvez modifier votre informations personnelles sur
ProConnect. +
+ - Si ProConnect vous permet de vous connecter avec un autre fournisseur d'identité, c'est à celui-ci de mettre + à jour vos informations personnelle. +
+ En cas de doute, vous pouvez contacter le support ProConnect +

+

Une fois modifiée, vos informations seront mises à jour à votre prochaine connexion.

+
+
+ {% bootstrap_form_errors form type="all" %} + {% elif ic_account_url %}