From 8a9a26c340ea983ad39e8082e27e19b689880f13 Mon Sep 17 00:00:00 2001
From: Andreas Ellewsen <ae@uio.no>
Date: Wed, 3 Nov 2021 12:49:13 +0100
Subject: [PATCH] Use hash in invite url

To prevent the invite id from showing up in various logs along the way,
we change to use an anchor in the invite url, and a post request to the
backend.

Invite links are now on the form /invitelink/#<uuid> and the frontend
redirects to /invite as usual after the session has been created.

Resolves: GREG-90
---
 frontend/src/routes/index.tsx            |  2 +-
 frontend/src/routes/invitelink/index.tsx |  9 +++----
 gregui/api/urls.py                       |  2 +-
 gregui/api/views/invitation.py           |  8 ++++--
 gregui/tests/api/test_invitation.py      | 34 +++++++-----------------
 5 files changed, 22 insertions(+), 33 deletions(-)

diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx
index 40b95593..3ad2e49a 100644
--- a/frontend/src/routes/index.tsx
+++ b/frontend/src/routes/index.tsx
@@ -64,7 +64,7 @@ export default function App() {
           <ProtectedRoute path="/register">
             <Register />
           </ProtectedRoute>
-          <Route path="/invite/:id" component={InviteLink} />
+          <Route path="/invitelink/" component={InviteLink} />
           <Route path="/invite/" component={Invite} />
           <Route path="/guestregister/" component={GuestRegister} />
           <Route>
diff --git a/frontend/src/routes/invitelink/index.tsx b/frontend/src/routes/invitelink/index.tsx
index 32dc16d0..eb839a24 100644
--- a/frontend/src/routes/invitelink/index.tsx
+++ b/frontend/src/routes/invitelink/index.tsx
@@ -1,16 +1,15 @@
 import { useEffect } from 'react'
-import { Redirect, useParams } from 'react-router-dom'
-
-type TParams = { id: string }
+import { Redirect } from 'react-router-dom'
+import { submitJsonOpts } from 'utils'
 
 function InviteLink() {
   // Fetch backend endpoint to preserve invite_id in backend session then redirect
   // to generic invite page with info about feide login or manual with passport.
 
-  const { id } = useParams<TParams>()
+  const id = window.location.hash.slice(1)
 
   useEffect(() => {
-    fetch(`/api/ui/v1/invited/${id}`)
+    fetch('/api/ui/v1/invitecheck/', submitJsonOpts('POST', { uuid: id }))
   }, [])
   return <Redirect to="/invite" />
 }
diff --git a/gregui/api/urls.py b/gregui/api/urls.py
index 46e1cf0f..49a4902f 100644
--- a/gregui/api/urls.py
+++ b/gregui/api/urls.py
@@ -20,7 +20,7 @@ urlpatterns += [
     re_path(r"roletypes/$", RoleTypeViewSet.as_view(), name="role-types"),
     re_path(r"units/$", UnitsViewSet.as_view(), name="units"),
     path("invited/", InvitedGuestView.as_view(), name="invited-info"),
-    path("invited/<uuid>", CheckInvitationView.as_view(), name="invite-verify"),
+    path("invitecheck/", CheckInvitationView.as_view(), name="invite-verify"),
     path("invite/", InvitationView.as_view(), name="invitation"),
     path("person/<int:id>", PersonView.as_view(), name="person-get"),
     path(
diff --git a/gregui/api/views/invitation.py b/gregui/api/views/invitation.py
index 2fd44854..de69519e 100644
--- a/gregui/api/views/invitation.py
+++ b/gregui/api/views/invitation.py
@@ -97,7 +97,7 @@ class CheckInvitationView(APIView):
     authentication_classes = []
     permission_classes = [AllowAny]
 
-    def get(self, request, *args, **kwargs):
+    def post(self, request, *args, **kwargs):
         """
         Endpoint for verifying and setting invite_id in session.
 
@@ -105,8 +105,12 @@ class CheckInvitationView(APIView):
         page you get to by following the invitation url to the frontend. This way a
         session is created keeping the invite id safe, until the user returns from
         feide login if they choose to use it.
+
+        Uses post to prevent invite id from showing up in various logs.
         """
-        invite_id = kwargs["uuid"]
+        invite_id = request.data.get("uuid")
+        if not invite_id:
+            return Response(status=status.HTTP_403_FORBIDDEN)
         try:
             invite_link = InvitationLink.objects.get(uuid=invite_id)
         except (InvitationLink.DoesNotExist, exceptions.ValidationError):
diff --git a/gregui/tests/api/test_invitation.py b/gregui/tests/api/test_invitation.py
index c9398418..99b6adc0 100644
--- a/gregui/tests/api/test_invitation.py
+++ b/gregui/tests/api/test_invitation.py
@@ -10,17 +10,15 @@ from greg.models import InvitationLink, Person, Identity
 @pytest.mark.django_db
 def test_get_invite(client):
     """Forbid access with bad invitation link uuid"""
-    response = client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": "baduuid"})
-    )
+    response = client.post(reverse("gregui-v1:invite-verify"), data={"uuid": "baduuid"})
     assert response.status_code == status.HTTP_403_FORBIDDEN
 
 
 @pytest.mark.django_db
 def test_get_invite_ok(client, invitation_link):
     """Access okay with valid invitation link"""
-    response = client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
+    response = client.post(
+        reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid}
     )
     assert response.status_code == status.HTTP_200_OK
 
@@ -28,10 +26,8 @@ def test_get_invite_ok(client, invitation_link):
 @pytest.mark.django_db
 def test_get_invite_expired(client, invitation_link_expired):
     """Forbid access with expired invite link"""
-    response = client.get(
-        reverse(
-            "gregui-v1:invite-verify", kwargs={"uuid": invitation_link_expired.uuid}
-        )
+    response = client.post(
+        reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link_expired.uuid}
     )
     assert response.status_code == status.HTTP_403_FORBIDDEN
 
@@ -47,9 +43,7 @@ def test_get_invited_info_session_okay(
     client, invitation_link, person_foo_data, sponsor_guy_data, role_type_foo, unit_foo
 ):
     # get a session
-    client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
-    )
+    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
     # Get info
     response = client.get(reverse("gregui-v1:invited-info"))
     assert response.status_code == status.HTTP_200_OK
@@ -82,9 +76,7 @@ def test_get_invited_info_expired_link(
     client, invitation_link, invitation_expired_date
 ):
     # Get a session while link is valid
-    client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
-    )
+    client.get(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
     # Set expire link to expire long ago
     invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
     invlink.expire = invitation_expired_date
@@ -100,9 +92,7 @@ def test_invited_guest_can_post_information(
     client: APIClient, invitation_link, person_foo_data
 ):
     # get a session
-    client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
-    )
+    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
 
     person = invitation_link.invitation.role.person
     assert person.private_mobile is None
@@ -129,9 +119,7 @@ def test_post_invited_info_expired_session(
     client, invitation_link, invitation_expired_date
 ):
     # get a session
-    client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
-    )
+    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
     # Set expire link to expire long ago
     invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
     invlink.expire = invitation_expired_date
@@ -145,9 +133,7 @@ def test_post_invited_info_expired_session(
 @pytest.mark.django_db
 def test_post_invited_info_deleted_inv_link(client, invitation_link):
     # get a session
-    client.get(
-        reverse("gregui-v1:invite-verify", kwargs={"uuid": invitation_link.uuid})
-    )
+    client.post(reverse("gregui-v1:invite-verify"), data={"uuid": invitation_link.uuid})
     # Delete link
     invlink = InvitationLink.objects.get(uuid=invitation_link.uuid)
     invlink.delete()
-- 
GitLab