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"""
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_TITLE = "consent_title"
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 @@ class ConsentStageView(ChallengeStageView):
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()

View File

@ -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.tests import FlowTestCase
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 @@ class TestConsentStage(FlowTestCase):
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 @@ class TestConsentStage(FlowTestCase):
)
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 @@ class TestConsentStage(FlowTestCase):
{},
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
raw_res = self.assertStageResponse(
response,
flow,
self.user,
@ -105,7 +115,9 @@ class TestConsentStage(FlowTestCase):
)
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 @@ class TestConsentStage(FlowTestCase):
{},
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
raw_res = self.assertStageResponse(
response,
flow,
self.user,
@ -155,7 +167,9 @@ class TestConsentStage(FlowTestCase):
)
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 @@ class TestConsentStage(FlowTestCase):
{},
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(
raw_res = self.assertStageResponse(
response,
flow,
self.user,
@ -200,7 +214,9 @@ class TestConsentStage(FlowTestCase):
)
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"))

View File

@ -20523,11 +20523,14 @@ components:
type: array
items:
$ref: '#/components/schemas/Permission'
token:
type: string
required:
- additional_permissions
- pending_user
- pending_user_avatar
- permissions
- token
- type
ConsentChallengeResponseRequest:
type: object
@ -20537,6 +20540,11 @@ components:
type: string
minLength: 1
default: ak-stage-consent
token:
type: string
minLength: 1
required:
- token
ConsentStage:
type: object
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 {
host!: StageHost;
@property({ attribute: false })
challenge!: Tin;
async submitForm(e: Event): Promise<boolean> {
async submitForm(e: Event, defaults?: KeyUnknown): Promise<boolean> {
e.preventDefault();
const object: {
[key: string]: unknown;
} = {};
const object: KeyUnknown = defaults || {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
for await (const [key, value] of form.entries()) {

View File

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