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:
parent
dae6493a3e
commit
01529d3894
|
@ -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()
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in New Issue