-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Populate ExistingUserLogin email if the user has proven knowledge
When the user proves knowledge of their email during the login flow, we store the email entered in the session so that we can pre-fill it for them on the subsequent password form. Not all flows leading to this view involve the user proving their email however, so if the key is not present in the session or it doesn't match the user's public id then it is ignored.
- Loading branch information
1 parent
2994889
commit b30a7a3
Showing
6 changed files
with
190 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY = "job_seeker_login_email" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
from itou.openid_connect.errors import format_error_modal_content | ||
from itou.users.enums import IdentityProvider | ||
from itou.users.models import User | ||
from itou.www.login.constants import ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY | ||
|
||
|
||
class FindExistingUserViaEmailForm(forms.Form): | ||
|
@@ -49,6 +50,7 @@ def clean_email(self): | |
extra_tags="modal login_failure email_does_not_exist", | ||
) | ||
raise ValidationError("Cette adresse e-mail est inconnue. Veuillez en saisir une autre, ou vous inscrire.") | ||
self.request.session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] = email | ||
return email | ||
|
||
|
||
|
@@ -57,15 +59,22 @@ class ItouLoginForm(LoginForm): | |
demo_banner_account = forms.BooleanField(widget=forms.HiddenInput(), required=False) | ||
|
||
def __init__(self, *args, **kwargs): | ||
user_email = kwargs.pop("user_email", None) | ||
super().__init__(*args, **kwargs) | ||
self.fields["password"].widget.attrs["placeholder"] = "**********" | ||
self.fields["password"].help_text = format_html( | ||
'<a href="{}" class="btn-link fs-sm">Mot de passe oublié ?</a>', | ||
reverse("account_reset_password"), | ||
) | ||
self.fields["login"].widget.attrs["placeholder"] = "[email protected]" | ||
self.fields["login"].label = "Adresse e-mail" | ||
self.fields["login"].widget.attrs["autofocus"] = True | ||
|
||
if user_email: | ||
self.fields["login"].initial = user_email | ||
self.fields["login"].widget.attrs["disabled"] = True | ||
self.data = self.data.dict() | {"login": user_email} | ||
else: | ||
self.fields["login"].widget.attrs["placeholder"] = "[email protected]" | ||
self.fields["login"].widget.attrs["autofocus"] = True | ||
|
||
def clean(self): | ||
# Parent method performs authentication on form success. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -236,6 +236,132 @@ | |
</div> | ||
|
||
|
||
</div> | ||
''' | ||
# --- | ||
# name: TestExistingUserLogin.test_login_email_prefilled[login_not_prefilled] | ||
''' | ||
<div class="c-form mb-5"> | ||
|
||
|
||
|
||
|
||
|
||
<form class="js-prevent-multiple-submit" method="post"> | ||
<input name="csrfmiddlewaretoken" type="hidden" value="NORMALIZED_CSRF_TOKEN"/> | ||
<fieldset> | ||
<p class="h4">Se connecter avec vos identifiants</p> | ||
|
||
|
||
<div class="form-group form-group-required"><label class="form-label" for="id_login">Adresse e-mail</label><input autocomplete="email" autofocus="" class="form-control" id="id_login" maxlength="320" name="login" placeholder="[email protected]" required="" type="email"/></div> | ||
<div class="mb-3 form-group-required"><label class="form-label" for="id_password">Mot de passe</label><div class="input-group"> | ||
<input aria-describedby="id_password_helptext" autocomplete="current-password" class="form-control" id="id_password" name="password" placeholder="**********" required="" type="password"/> | ||
|
||
<div class="input-group-text p-0"> | ||
<button class="btn btn-sm btn-link btn-ico" data-it-password="toggle" type="button"> | ||
<i aria-hidden="true" class="ri-eye-line"></i> | ||
<span>Afficher</span> | ||
</button> | ||
</div> | ||
</div><div class="form-text"><a class="btn-link fs-sm" href="/accounts/password/reset/">Mot de passe oublié ?</a></div> | ||
</div> | ||
</fieldset> | ||
|
||
|
||
|
||
<div class="row"> | ||
<div class="col-12"> | ||
<hr class="mb-3"/> | ||
<small class="d-inline-block mb-3">* champs obligatoires</small> | ||
<div class="form-row align-items-center justify-content-end gx-3"> | ||
<div class="form-group col-12 col-lg order-3 order-lg-1"> | ||
|
||
<a aria-label="Annuler la saisie de ce formulaire" class="btn btn-link btn-ico ps-lg-0 w-100 w-lg-auto" href="/"> | ||
<i aria-hidden="true" class="ri-close-line ri-lg"></i> | ||
<span>Annuler</span> | ||
</a> | ||
|
||
</div> | ||
|
||
<div class="form-group col col-lg-auto order-2 order-lg-3"> | ||
|
||
<button aria-label="Passer à l’étape suivante" class="btn btn-block btn-primary" type="submit"> | ||
<span>Se connecter</span> | ||
</button> | ||
|
||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
|
||
|
||
</form> | ||
|
||
|
||
</div> | ||
''' | ||
# --- | ||
# name: TestExistingUserLogin.test_login_email_prefilled[login_prefilled] | ||
''' | ||
<div class="c-form mb-5"> | ||
|
||
|
||
|
||
|
||
|
||
<form class="js-prevent-multiple-submit" method="post"> | ||
<input name="csrfmiddlewaretoken" type="hidden" value="NORMALIZED_CSRF_TOKEN"/> | ||
<fieldset> | ||
<p class="h4">Se connecter avec vos identifiants</p> | ||
|
||
|
||
<div class="form-group form-group-required"><label class="form-label" for="id_login">Adresse e-mail</label><input autocomplete="email" class="form-control" disabled="" id="id_login" maxlength="320" name="login" placeholder="Adresse e-mail" required="" type="email" value="[email protected]"/></div> | ||
<div class="mb-3 form-group-required"><label class="form-label" for="id_password">Mot de passe</label><div class="input-group"> | ||
<input aria-describedby="id_password_helptext" autocomplete="current-password" class="form-control" id="id_password" name="password" placeholder="**********" required="" type="password"/> | ||
|
||
<div class="input-group-text p-0"> | ||
<button class="btn btn-sm btn-link btn-ico" data-it-password="toggle" type="button"> | ||
<i aria-hidden="true" class="ri-eye-line"></i> | ||
<span>Afficher</span> | ||
</button> | ||
</div> | ||
</div><div class="form-text"><a class="btn-link fs-sm" href="/accounts/password/reset/">Mot de passe oublié ?</a></div> | ||
</div> | ||
</fieldset> | ||
|
||
|
||
|
||
<div class="row"> | ||
<div class="col-12"> | ||
<hr class="mb-3"/> | ||
<small class="d-inline-block mb-3">* champs obligatoires</small> | ||
<div class="form-row align-items-center justify-content-end gx-3"> | ||
<div class="form-group col-12 col-lg order-3 order-lg-1"> | ||
|
||
<a aria-label="Annuler la saisie de ce formulaire" class="btn btn-link btn-ico ps-lg-0 w-100 w-lg-auto" href="/"> | ||
<i aria-hidden="true" class="ri-close-line ri-lg"></i> | ||
<span>Annuler</span> | ||
</a> | ||
|
||
</div> | ||
|
||
<div class="form-group col col-lg-auto order-2 order-lg-3"> | ||
|
||
<button aria-label="Passer à l’étape suivante" class="btn btn-block btn-primary" type="submit"> | ||
<span>Se connecter</span> | ||
</button> | ||
|
||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
|
||
|
||
</form> | ||
|
||
|
||
</div> | ||
''' | ||
# --- | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
from itou.users.enums import IdentityProvider, UserKind | ||
from itou.utils import constants as global_constants | ||
from itou.utils.urls import add_url_params | ||
from itou.www.login.constants import ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY | ||
from itou.www.login.forms import ItouLoginForm | ||
from itou.www.login.views import ExistingUserLoginView | ||
from tests.openid_connect.france_connect.tests import FC_USERINFO, mock_oauth_dance | ||
|
@@ -225,6 +226,9 @@ def test_pre_login_redirects_to_existing_user(self, client): | |
response = client.post(url, data=form_data) | ||
assertRedirects(response, f'{reverse("login:existing_user", args=(user.public_id,))}?back_url={url}') | ||
|
||
# Email is populated in session. The utility of this is covered by the ExistingUserLoginView tests. | ||
assert client.session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] == user.email | ||
|
||
def test_pre_login_email_unknown(self, client, snapshot): | ||
url = reverse("login:job_seeker") | ||
response = client.get(url) | ||
|
@@ -320,6 +324,36 @@ def test_login_django(self, client): | |
response = client.post(url, data=form_data) | ||
assertRedirects(response, reverse("account_email_verification_sent")) | ||
|
||
def test_login_email_prefilled(self, client, snapshot): | ||
# Login is not pre-filled just by visiting the page. | ||
# The user must prove they know this information | ||
user = JobSeekerFactory(identity_provider=IdentityProvider.DJANGO, for_snapshot=True) | ||
url = reverse("login:existing_user", args=(user.public_id,)) | ||
response = client.get(url) | ||
assert response.status_code == 200 | ||
|
||
assert response.context["form"]["login"].initial is None | ||
assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot(name="login_not_prefilled") | ||
|
||
# If the email has been populated in the session, but the email populated does not match the user requested, | ||
# then it is ignored. | ||
session = client.session | ||
session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] = "[email protected]" | ||
session.save() | ||
response = client.get(url) | ||
assert response.status_code == 200 | ||
assert response.context["form"]["login"].initial is None | ||
assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot(name="login_not_prefilled") | ||
|
||
# If the login has been populated in the session with the correct email, | ||
# then the user will not need to re-enter it a second time. | ||
session[ITOU_SESSION_JOB_SEEKER_LOGIN_EMAIL_KEY] = user.email | ||
session.save() | ||
response = client.get(url) | ||
assert response.status_code == 200 | ||
assert response.context["form"]["login"].initial == user.email | ||
assert str(parse_response_to_soup(response, selector=".c-form")) == snapshot(name="login_prefilled") | ||
|
||
@pytest.mark.parametrize( | ||
"identity_provider", | ||
[ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters