Repository: e-valuation/EvaP

Issue:
Add confirmation for login with login key
External users can login by clicking on the login URL sent to them via email. Before users are actually authenticated on the platform after clicking on this link, they should have to confirm the login by, e.g., clicking a confirmation button in a modal or on a designated page.

This prevents cases in which the URL is already requested for, e.g., a preview of the page and thus invalidates the login key which results in a new link being sent when users actively click on the link (which is then already the second GET request).


Discussion:
Why not invalidate the key when navigating away from the page the invite link leads to? That way, there is no additional step.
then we'd have to add authentication logic on some random page which doesn't seem optimal. i think one click on "I want to log in as John Doe" is perfectly fine.
fwiw, https://stackoverflow.com/questions/41699071/bingpreview-invalidates-one-time-links-in-email/42525744. i suspect that's the reason why this is happening and personally i would prefer a targeted solution, and not degrade the experience of all because some email client is misbehaving.

Resolving patch:
diff --git a/evap/evaluation/views.py b/evap/evaluation/views.py
--- a/evap/evaluation/views.py
+++ b/evap/evaluation/views.py
@@ -100,6 +100,12 @@ def login_key_authentication(request, key):
         return redirect('evaluation:index')
 
     if user and user.login_key_valid_until >= date.today():
+        if request.method != "POST":
+            template_data = {
+                'username': user.full_name
+            }
+            return render(request, "external_user_confirm_login.html", template_data)
+
         # User is valid. Set request.user and persist user in the session by logging the user in.
         request.user = user
         auth.login(request, user)


Test patch:
diff --git a/evap/evaluation/tests/test_auth.py b/evap/evaluation/tests/test_auth.py
--- a/evap/evaluation/tests/test_auth.py
+++ b/evap/evaluation/tests/test_auth.py
@@ -33,22 +33,36 @@ def test_login_url_works(self):
         self.assertRedirects(self.app.get(reverse("contributor:index")), "/?next=/contributor/")
 
         url_with_key = reverse("evaluation:login_key_authentication", args=[self.external_user.login_key])
-        page = self.app.get(url_with_key).follow().follow()
+        old_login_key = self.external_user.login_key
+        old_login_key_valid_until = self.external_user.login_key_valid_until
+        page = self.app.get(url_with_key)
+        self.external_user.refresh_from_db()
+        self.assertEqual(old_login_key, self.external_user.login_key)
+        self.assertEqual(old_login_key_valid_until, self.external_user.login_key_valid_until)
+        self.assertContains(page, 'Login')
+        self.assertContains(page, self.external_user.full_name)
+
+        page = self.app.post(url_with_key).follow().follow()
         self.assertContains(page, 'Logout')
+        self.assertContains(page, self.external_user.full_name)
 
     def test_login_key_valid_only_once(self):
-        page = self.app.get(reverse("evaluation:login_key_authentication", args=[self.external_user.login_key])).follow().follow()
+        page = self.app.get(reverse("evaluation:login_key_authentication", args=[self.external_user.login_key]))
         self.assertContains(page, self.external_user.full_name)
 
+        url_with_key = reverse("evaluation:login_key_authentication", args=[self.external_user.login_key])
+        page = self.app.post(url_with_key).follow().follow()
+        self.assertContains(page, 'Logout')
+
         page = self.app.get(reverse("django-auth-logout")).follow()
         self.assertNotContains(page, 'Logout')
 
-        page = self.app.get(reverse("evaluation:login_key_authentication", args=[self.external_user.login_key])).follow()
+        page = self.app.get(url_with_key).follow()
         self.assertContains(page, 'The login URL is not valid anymore.')
         self.assertEqual(len(mail.outbox), 1)  # a new login key was sent
 
         new_key = UserProfile.objects.get(id=self.external_user.id).login_key
-        page = self.app.get(reverse("evaluation:login_key_authentication", args=[new_key])).follow().follow()
+        page = self.app.post(reverse("evaluation:login_key_authentication", args=[new_key])).follow().follow()
         self.assertContains(page, self.external_user.full_name)
 
     def test_inactive_external_users_can_not_login(self):