flows/stages/consent: fix for post requests (#3339)

add unique token to consent stage to ensure it is shown

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens L 2022-07-31 23:47:40 +02:00 committed by Jens Langhammer
parent dae6493a3e
commit 01529d3894
5 changed files with 51 additions and 14 deletions

View File

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

View File

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

View File

@ -20523,11 +20523,14 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/Permission' $ref: '#/components/schemas/Permission'
token:
type: string
required: required:
- additional_permissions - additional_permissions
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- permissions - permissions
- token
- type - type
ConsentChallengeResponseRequest: ConsentChallengeResponseRequest:
type: object type: object
@ -20537,6 +20540,11 @@ components:
type: string type: string
minLength: 1 minLength: 1
default: ak-stage-consent default: ak-stage-consent
token:
type: string
minLength: 1
required:
- token
ConsentStage: ConsentStage:
type: object type: object
description: ConsentStage Serializer description: ConsentStage Serializer

View File

@ -23,17 +23,19 @@ export function readFileAsync(file: Blob) {
}); });
} }
export type KeyUnknown = {
[key: string]: unknown;
};
export class BaseStage<Tin, Tout> extends LitElement { export class BaseStage<Tin, Tout> extends LitElement {
host!: StageHost; host!: StageHost;
@property({ attribute: false }) @property({ attribute: false })
challenge!: Tin; challenge!: Tin;
async submitForm(e: Event): Promise<boolean> { async submitForm(e: Event, defaults?: KeyUnknown): Promise<boolean> {
e.preventDefault(); e.preventDefault();
const object: { const object: KeyUnknown = defaults || {};
[key: string]: unknown;
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined); const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
for await (const [key, value] of form.entries()) { for await (const [key, value] of form.entries()) {

View File

@ -107,7 +107,9 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
<form <form
class="pf-c-form" class="pf-c-form"
@submit=${(e: Event) => { @submit=${(e: Event) => {
this.submitForm(e); this.submitForm(e, {
token: this.challenge.token,
});
}} }}
> >
<ak-form-static <ak-form-static