auth_fc: account verification view (#73148)

This commit is contained in:
Paul Marillonnet 2023-01-17 17:26:41 +01:00
parent 6e9590e4c8
commit 25aec13ee8
3 changed files with 125 additions and 0 deletions

View File

@ -14,6 +14,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.auth.decorators import login_required
from django.urls import include, path, re_path
from . import views
@ -21,6 +22,11 @@ from . import views
fcpatterns = [
path('callback/', views.login_or_link, name='fc-login-or-link'),
path('callback_logout/', views.logout, name='fc-logout'),
re_path(
r'^verify/(?P<token>[A-Za-z0-9_ -]+)/$',
login_required(views.FcEmailVerificationView.as_view()),
name='fc-verification',
),
]
urlpatterns = [

View File

@ -32,6 +32,7 @@ from django.template.loader import render_to_string
from django.template.response import TemplateResponse
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.timezone import now
from django.utils.translation import gettext as _
from django.views.generic import FormView, View
from requests_oauthlib import OAuth2Session
@ -673,3 +674,37 @@ class LogoutReturnView(View):
logout = LogoutReturnView.as_view()
class FcEmailVerificationView(View):
def get(self, request, *args, **kwargs):
token_value = kwargs.get('token')
try:
token = models.FcEmailVerificationToken.objects.get(value=token_value)
except models.FcEmailVerificationToken.DoesNotExist:
messages.error(
request,
_('Invalid account verification request.'),
)
return utils_misc.redirect(request, 'account_management')
if token.user != request.user:
messages.error(
request,
_('Invalid account verification request.'),
)
return utils_misc.redirect(request, 'account_management')
if token.expires < now():
messages.error(
request,
_('Your account verification request has expired.'),
)
return utils_misc.redirect(request, 'account_management')
token.user.set_email_verified(True, source='fc')
messages.info(
request,
_('Your account is now verified.'),
)
return utils_misc.redirect(request, 'account_management')

View File

@ -112,6 +112,7 @@ def test_create(settings, app, franceconnect, hooks, service, mailoutbox):
assert User.objects.count() == 0
assert Event.objects.which_references(service).count() == 0
assert models.FcEmailVerificationToken.objects.count() == 0
response = franceconnect.handle_authorization(app, response.location, status=302)
assert 'fc-state' not in app.cookies
assert User.objects.count() == 1
@ -125,10 +126,14 @@ def test_create(settings, app, franceconnect, hooks, service, mailoutbox):
# check registration email
assert len(mailoutbox) == 1
assert mailoutbox[0].subject == 'Account creation using FranceConnect'
assert models.FcEmailVerificationToken.objects.count() == 1
token = models.FcEmailVerificationToken.objects.get()
assert len(str(token.value)) == 36
for body in (mailoutbox[0].body, mailoutbox[0].alternatives[0][0]):
assert 'Hi Ÿuñe Frédérique,' in body
assert 'You have just created an account using FranceConnect.' in body
assert 'You can complete your account validation' in body
assert f'https://testserver/fc/verify/{token.value}/' in body
assert user.verified_attributes.first_name == 'Ÿuñe'
assert user.verified_attributes.last_name == 'Frédérique'
@ -140,6 +145,13 @@ def test_create(settings, app, franceconnect, hooks, service, mailoutbox):
assert last_name_value.last_verified_on
assert first_name_value.verification_sources == ['fc']
assert last_name_value.verification_sources == ['fc']
assert not user.email_verified
resp = app.get(f'https://testserver/fc/verify/{token.value}/').follow()
assert resp.pyquery('li.info').text() == 'Your account is now verified.'
user.refresh_from_db()
assert user.email_verified
assert path(response.location) == '/idp/'
assert hooks.event[1]['kwargs']['name'] == 'login'
assert hooks.event[1]['kwargs']['service'] == service
@ -170,6 +182,78 @@ def test_create(settings, app, franceconnect, hooks, service, mailoutbox):
assert 'Your account link to FranceConnect has been deleted' in response
def test_email_verification_anonymous_user(app, simple_user):
token = models.FcEmailVerificationToken.create(user=simple_user)
token.sent = True
token.save()
resp = app.get(f'https://testserver/fc/verify/{token.value}/')
assert resp.location == f'/login/?next=/fc/verify/{token.value}/'
simple_user.refresh_from_db()
assert not simple_user.email_verified
resp = resp.follow()
resp.form.set('username', simple_user.username)
resp.form.set('password', simple_user.username)
resp = resp.form.submit(name='login-password-submit')
assert resp.location == f'/fc/verify/{token.value}/'
resp = resp.follow()
assert resp.location == '/accounts/'
resp = resp.follow()
assert resp.pyquery('li.info').text() == 'Your account is now verified.'
simple_user.refresh_from_db()
assert simple_user.email_verified
def test_email_verification_wrong_link(settings, app, franceconnect, hooks, service):
set_service(app, service)
response = app.get('/login/?next=/idp/')
response = response.click(href='callback')
franceconnect.handle_authorization(app, response.location, status=302)
user = User.objects.get()
# user is logged yet clicks on a wrong link
resp = app.get('https://testserver/fc/verify/01234567-aaaa-bbbb-cccc-abcdabdcabdc/').follow()
assert resp.pyquery('li.error').text() == 'Invalid account verification request.'
user.refresh_from_db()
assert not user.email_verified
def test_email_verification_expired(settings, app, franceconnect, hooks, service, freezer):
set_service(app, service)
response = app.get('/login/?next=/idp/')
response = response.click(href='callback')
response = franceconnect.handle_authorization(app, response.location, status=302)
user = User.objects.get()
token = models.FcEmailVerificationToken.objects.get()
assert not user.email_verified
freezer.move_to(datetime.timedelta(hours=50)) # too late by two hours
resp = app.get(f'https://testserver/fc/verify/{token.value}/').follow()
assert not resp.pyquery('li.info')
assert resp.pyquery('li.error').text() == 'Your account verification request has expired.'
user.refresh_from_db()
assert not user.email_verified
def test_email_verification_wrong_user(settings, app, franceconnect, user_ou1, hooks, service, mailoutbox):
set_service(app, service)
response = app.get('/login/?next=/idp/')
response = response.click(href='callback')
response = franceconnect.handle_authorization(app, response.location, status=302)
user = User.objects.get(ou=get_default_ou())
token = models.FcEmailVerificationToken.objects.get()
token.user = user_ou1
token.save()
assert not user.email_verified
resp = app.get(f'https://testserver/fc/verify/{token.value}/').follow()
assert not resp.pyquery('li.info')
assert resp.pyquery('li.error').text() == 'Invalid account verification request.'
user.refresh_from_db()
assert not user.email_verified
def test_create_no_unicode_collision(settings, app, franceconnect, hooks, service):
settings.A2_EMAIL_IS_UNIQUE = True
set_service(app, service)