Repository: goauthentik/authentik

Issue:
Consent stage shown every time: Something went wrong! Please try again later. (DB Key already exists)
**Describe the bug**
The consent stage is shown every time, although consent was already given and did not expire, which leads to an exception.

**To Reproduce**
Steps to reproduce the behavior:
1. Log into authentik
2. Click on an application
3. Give consent
4. Log out
5. Click on the same application
6. Consent stage will appear again
7. Giving consent leads into: "Whoops! Something went wrong! Please try again later."

**Expected behavior**
Consent stage should only be shown once in x days (as configured) and not always. Did work perfectly in versions before authentik 2022.7.

**Logs**
<details>
    <summary>Stacktrace from authentik</summary>

```
Traceback (most recent call last):
  File "/authentik/flows/views/executor.py", line 337, in post
    stage_response = self.current_stage_view.post(request, *args, **kwargs)
  File "/authentik/flows/stage.py", line 121, in post
    return self.challenge_valid(challenge)
  File "/authentik/stages/consent/stage.py", line 127, in challenge_valid
    UserConsent.objects.create(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 514, in create
    obj.save(force_insert=True, using=self.db)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 806, in save
    self.save_base(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 857, in save_base
    updated = self._save_table(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 1000, in _save_table
    results = self._do_insert(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 1041, in _do_insert
    return manager._insert(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1434, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1621, in execute_sql
    cursor.execute(sql, params)
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/usr/local/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.10/site-packages/django_prometheus/db/common.py", line 71, in execute
    return super().execute(*args, **kwargs)
django.db.utils.IntegrityError: duplicate key value violates unique constraint "authentik_stages_consent_user_id_application_id_p_e44c6458_uniq"
DETAIL:  Key (user_id, application_id, permissions)=(79, 86534e2a-befb-40ae-8581-c2387b4ba06b, ) already exists.

```
</details>


**Version and Deployment (please complete the following information):**
- authentik version: 2022.7.2
- Deployment: docker-compose
        


Discussion:
fixed by https://github.com/goauthentik/authentik/commit/9a9ba2560b2d607442905aabbcc5284bb68b5489, included in 2022.7.3
@BeryJu I am still hitting this same error on 2022.7.3. I upgraded straight from 2022.6.x. Same steps to reproduce. Should I open another issue?

<details>
<summary>
Stacktrace
</summary>

```
  File "/authentik/flows/views/executor.py", line 337, in post
    stage_response = self.current_stage_view.post(request, *args, **kwargs)
  File "/authentik/flows/stage.py", line 121, in post
    return self.challenge_valid(challenge)
  File "/authentik/stages/consent/stage.py", line 120, in challenge_valid
    UserConsent.objects.create(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 514, in create
    obj.save(force_insert=True, using=self.db)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 806, in save
    self.save_base(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 857, in save_base
    updated = self._save_table(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 1000, in _save_table
    results = self._do_insert(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/base.py", line 1041, in _do_insert
    return manager._insert(
  File "/usr/local/lib/python3.10/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/query.py", line 1434, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/usr/local/lib/python3.10/site-packages/django/db/models/sql/compiler.py", line 1621, in execute_sql
    cursor.execute(sql, params)
  File "/usr/local/lib/python3.10/site-packages/sentry_sdk/integrations/django/__init__.py", line 556, in execute
    return real_execute(self, sql, params)
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 67, in execute
    return self._execute_with_wrappers(
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 80, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 84, in _execute
    with self.db.wrap_database_errors:
  File "/usr/local/lib/python3.10/site-packages/django/db/utils.py", line 91, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/local/lib/python3.10/site-packages/django/db/backends/utils.py", line 89, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/local/lib/python3.10/site-packages/django_prometheus/db/common.py", line 71, in execute
    return super().execute(*args, **kwargs)
django.db.utils.IntegrityError: duplicate key value violates unique constraint "authentik_stages_consent_user_id_application_id_p_e44c6458_uniq"
DETAIL:  Key (user_id, application_id, permissions)=(4, 4116b732-78ba-4441-ab31-f10a2744c96a, email profile) already exists.
```

</details>

authentik version: 2022.7.3
Deployment: docker-compose

Thank you. 

Resolving patch:
diff --git a/authentik/stages/consent/stage.py b/authentik/stages/consent/stage.py
--- a/authentik/stages/consent/stage.py
+++ b/authentik/stages/consent/stage.py
@@ -1,5 +1,6 @@
 """authentik consent stage"""
 from typing import Optional
+from uuid import uuid4
 
 from django.http import HttpRequest, HttpResponse
 from django.utils.timezone import now
@@ -21,6 +22,7 @@
 PLAN_CONTEXT_CONSENT_HEADER = "consent_header"
 PLAN_CONTEXT_CONSENT_PERMISSIONS = "consent_permissions"
 PLAN_CONTEXT_CONSNET_EXTRA_PERMISSIONS = "consent_additional_permissions"
+SESSION_KEY_CONSENT_TOKEN = "authentik/stages/consent/token"  # nosec
 
 
 class ConsentChallenge(WithUserInfoChallenge):
@@ -30,12 +32,14 @@ class ConsentChallenge(WithUserInfoChallenge):
     permissions = PermissionSerializer(many=True)
     additional_permissions = PermissionSerializer(many=True)
     component = CharField(default="ak-stage-consent")
+    token = CharField(required=True)
 
 
 class ConsentChallengeResponse(ChallengeResponse):
     """Consent challenge response, any valid response request is valid"""
 
     component = CharField(default="ak-stage-consent")
+    token = CharField(required=True)
 
 
 class ConsentStageView(ChallengeStageView):
@@ -44,12 +48,15 @@ class ConsentStageView(ChallengeStageView):
     response_class = ConsentChallengeResponse
 
     def get_challenge(self) -> Challenge:
+        token = str(uuid4())
+        self.request.session[SESSION_KEY_CONSENT_TOKEN] = token
         data = {
             "type": ChallengeTypes.NATIVE.value,
             "permissions": self.executor.plan.context.get(PLAN_CONTEXT_CONSENT_PERMISSIONS, []),
             "additional_permissions": self.executor.plan.context.get(
                 PLAN_CONTEXT_CONSNET_EXTRA_PERMISSIONS, []
             ),
+            "token": token,
         }
         if PLAN_CONTEXT_CONSENT_TITLE in self.executor.plan.context:
             data["title"] = self.executor.plan.context[PLAN_CONTEXT_CONSENT_TITLE]
@@ -102,6 +109,8 @@ def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
         return super().get(request, *args, **kwargs)
 
     def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
+        if response.data["token"] != self.request.session[SESSION_KEY_CONSENT_TOKEN]:
+            return self.get(self.request)
         current_stage: ConsentStage = self.executor.current_stage
         if PLAN_CONTEXT_APPLICATION not in self.executor.plan.context:
             return self.executor.stage_ok()


Test patch:
diff --git a/authentik/stages/consent/tests.py b/authentik/stages/consent/tests.py
--- a/authentik/stages/consent/tests.py
+++ b/authentik/stages/consent/tests.py
@@ -1,5 +1,6 @@
 """consent tests"""
 from time import sleep
+from uuid import uuid4
 
 from django.urls import reverse
 
@@ -14,7 +15,10 @@
 from authentik.flows.views.executor import SESSION_KEY_PLAN
 from authentik.lib.generators import generate_id
 from authentik.stages.consent.models import ConsentMode, ConsentStage, UserConsent
-from authentik.stages.consent.stage import PLAN_CONTEXT_CONSENT_PERMISSIONS
+from authentik.stages.consent.stage import (
+    PLAN_CONTEXT_CONSENT_PERMISSIONS,
+    SESSION_KEY_CONSENT_TOKEN,
+)
 
 
 class TestConsentStage(FlowTestCase):
@@ -37,10 +41,13 @@ def test_always_required(self):
         plan = FlowPlan(flow_pk=flow.pk.hex, bindings=[binding], markers=[StageMarker()])
         session = self.client.session
         session[SESSION_KEY_PLAN] = plan
+        session[SESSION_KEY_CONSENT_TOKEN] = str(uuid4())
         session.save()
         response = self.client.post(
             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
-            {},
+            {
+                "token": session[SESSION_KEY_CONSENT_TOKEN],
+            },
         )
         # pylint: disable=no-member
         self.assertEqual(response.status_code, 200)
@@ -62,10 +69,13 @@ def test_permanent(self):
         )
         session = self.client.session
         session[SESSION_KEY_PLAN] = plan
+        session[SESSION_KEY_CONSENT_TOKEN] = str(uuid4())
         session.save()
         response = self.client.post(
             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
-            {},
+            {
+                "token": session[SESSION_KEY_CONSENT_TOKEN],
+            },
         )
         self.assertEqual(response.status_code, 200)
         self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@@ -96,7 +106,7 @@ def test_expire(self):
             {},
         )
         self.assertEqual(response.status_code, 200)
-        self.assertStageResponse(
+        raw_res = self.assertStageResponse(
             response,
             flow,
             self.user,
@@ -105,7 +115,9 @@ def test_expire(self):
         )
         response = self.client.post(
             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
-            {},
+            {
+                "token": raw_res["token"],
+            },
         )
         self.assertEqual(response.status_code, 200)
         self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@@ -144,7 +156,7 @@ def test_permanent_more_perms(self):
             {},
         )
         self.assertEqual(response.status_code, 200)
-        self.assertStageResponse(
+        raw_res = self.assertStageResponse(
             response,
             flow,
             self.user,
@@ -155,7 +167,9 @@ def test_permanent_more_perms(self):
         )
         response = self.client.post(
             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
-            {},
+            {
+                "token": raw_res["token"],
+            },
         )
         self.assertEqual(response.status_code, 200)
         self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
@@ -187,7 +201,7 @@ def test_permanent_more_perms(self):
             {},
         )
         self.assertEqual(response.status_code, 200)
-        self.assertStageResponse(
+        raw_res = self.assertStageResponse(
             response,
             flow,
             self.user,
@@ -200,7 +214,9 @@ def test_permanent_more_perms(self):
         )
         response = self.client.post(
             reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
-            {},
+            {
+                "token": raw_res["token"],
+            },
         )
         self.assertEqual(response.status_code, 200)
         self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))