flows: add default challenge response

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-05-24 20:04:56 +02:00
parent fb4e0723ee
commit c6bb6709fd
12 changed files with 109 additions and 46 deletions

View file

@ -83,7 +83,7 @@ class ChallengeResponse(PassiveSerializer):
"""Base class for all challenge responses""" """Base class for all challenge responses"""
stage: Optional["StageView"] stage: Optional["StageView"]
component = CharField(default="") component = CharField(default="xak-flow-response-default")
def __init__(self, instance=None, data=None, **kwargs): def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None) self.stage = kwargs.pop("stage", None)

View file

@ -215,7 +215,7 @@ class FlowExecutorView(APIView):
), ),
}, },
request=PolymorphicProxySerializer( request=PolymorphicProxySerializer(
component_name="ChallengeResponse", component_name="FlowChallengeResponse",
serializers=challenge_response_types(), serializers=challenge_response_types(),
resource_type_field_name="component", resource_type_field_name="component",
), ),

View file

@ -37,12 +37,20 @@ class AutosubmitChallenge(Challenge):
component = CharField(default="ak-stage-autosubmit") component = CharField(default="ak-stage-autosubmit")
class AutoSubmitChallengeResponse(ChallengeResponse):
"""Pseudo class for autosubmit response"""
component = CharField(default="ak-stage-autosubmit")
# This View doesn't have a URL on purpose, as its called by the FlowExecutor # This View doesn't have a URL on purpose, as its called by the FlowExecutor
class SAMLFlowFinalView(ChallengeStageView): class SAMLFlowFinalView(ChallengeStageView):
"""View used by FlowExecutor after all stages have passed. Logs the authorization, """View used by FlowExecutor after all stages have passed. Logs the authorization,
and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element and redirects to the SP (if REDIRECT is configured) or shows an auto-submit element
(if POST is configured).""" (if POST is configured)."""
response_class = AutoSubmitChallengeResponse
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION] application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
provider: SAMLProvider = get_object_or_404( provider: SAMLProvider = get_object_or_404(

View file

@ -8,7 +8,7 @@ from rest_framework.serializers import BaseSerializer
from authentik.core.models import Source, UserSourceConnection from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton from authentik.core.types import UILoginButton
from authentik.flows.challenge import Challenge, ChallengeTypes from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.providers.oauth2.generators import generate_client_id from authentik.providers.oauth2.generators import generate_client_id
@ -20,6 +20,12 @@ class PlexAuthenticationChallenge(Challenge):
component = CharField(default="ak-flow-sources-plex") component = CharField(default="ak-flow-sources-plex")
class PlexAuthenticationChallengeResponse(ChallengeResponse):
"""Pseudo class for plex response"""
component = CharField(default="ak-flow-sources-plex")
class PlexSource(Source): class PlexSource(Source):
"""Authenticate against plex.tv""" """Authenticate against plex.tv"""

View file

@ -28,9 +28,17 @@ class AuthenticatorDuoChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-authenticator-duo") component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoChallengeResponse(ChallengeResponse):
"""Pseudo class for duo response"""
component = CharField(default="ak-stage-authenticator-duo")
class AuthenticatorDuoStageView(ChallengeStageView): class AuthenticatorDuoStageView(ChallengeStageView):
"""Duo stage""" """Duo stage"""
response_class = AuthenticatorDuoChallengeResponse
def get_challenge(self, *args, **kwargs) -> Challenge: def get_challenge(self, *args, **kwargs) -> Challenge:
user = self.get_pending_user() user = self.get_pending_user()
stage: AuthenticatorDuoStage = self.executor.current_stage stage: AuthenticatorDuoStage = self.executor.current_stage

View file

@ -25,9 +25,17 @@ class AuthenticatorStaticChallenge(WithUserInfoChallenge):
component = CharField(default="ak-stage-authenticator-static") component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticChallengeResponse(ChallengeResponse):
"""Pseudo class for static response"""
component = CharField(default="ak-stage-authenticator-static")
class AuthenticatorStaticStageView(ChallengeStageView): class AuthenticatorStaticStageView(ChallengeStageView):
"""Static OTP Setup stage""" """Static OTP Setup stage"""
response_class = AuthenticatorStaticChallengeResponse
def get_challenge(self, *args, **kwargs) -> AuthenticatorStaticChallenge: def get_challenge(self, *args, **kwargs) -> AuthenticatorStaticChallenge:
tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS] tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS]
return AuthenticatorStaticChallenge( return AuthenticatorStaticChallenge(

View file

@ -3550,13 +3550,13 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ChallengeResponseRequest' $ref: '#/components/schemas/FlowChallengeResponseRequest'
application/x-www-form-urlencoded: application/x-www-form-urlencoded:
schema: schema:
$ref: '#/components/schemas/ChallengeResponseRequest' $ref: '#/components/schemas/FlowChallengeResponseRequest'
multipart/form-data: multipart/form-data:
schema: schema:
$ref: '#/components/schemas/ChallengeResponseRequest' $ref: '#/components/schemas/FlowChallengeResponseRequest'
security: security:
- authentik: [] - authentik: []
- cookieAuth: [] - cookieAuth: []
@ -15194,6 +15194,13 @@ components:
- pending_user_avatar - pending_user_avatar
- stage_uuid - stage_uuid
- type - type
AuthenticatorDuoChallengeResponseRequest:
type: object
description: Pseudo class for duo response
properties:
component:
type: string
default: ak-stage-authenticator-duo
AuthenticatorDuoStage: AuthenticatorDuoStage:
type: object type: object
description: AuthenticatorDuoStage Serializer description: AuthenticatorDuoStage Serializer
@ -15296,6 +15303,13 @@ components:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- type - type
AuthenticatorStaticChallengeResponseRequest:
type: object
description: Pseudo class for static response
properties:
component:
type: string
default: ak-stage-authenticator-static
AuthenticatorStaticStage: AuthenticatorStaticStage:
type: object type: object
description: AuthenticatorStaticStage Serializer description: AuthenticatorStaticStage Serializer
@ -15624,6 +15638,13 @@ components:
additionalProperties: {} additionalProperties: {}
required: required:
- response - response
AutoSubmitChallengeResponseRequest:
type: object
description: Pseudo class for autosubmit response
properties:
component:
type: string
default: ak-stage-autosubmit
AutosubmitChallenge: AutosubmitChallenge:
type: object type: object
description: Autosubmit challenge used to send and navigate a POST request description: Autosubmit challenge used to send and navigate a POST request
@ -15889,31 +15910,6 @@ components:
- shell - shell
- redirect - redirect
type: string type: string
ChallengeResponseRequest:
oneOf:
- $ref: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorValidationChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorWebAuthnChallengeResponseRequest'
- $ref: '#/components/schemas/CaptchaChallengeResponseRequest'
- $ref: '#/components/schemas/ConsentChallengeResponseRequest'
- $ref: '#/components/schemas/DummyChallengeResponseRequest'
- $ref: '#/components/schemas/EmailChallengeResponseRequest'
- $ref: '#/components/schemas/IdentificationChallengeResponseRequest'
- $ref: '#/components/schemas/PasswordChallengeResponseRequest'
- $ref: '#/components/schemas/PromptResponseChallengeRequest'
discriminator:
propertyName: component
mapping:
ak-stage-authenticator-totp: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest'
ak-stage-authenticator-validate: '#/components/schemas/AuthenticatorValidationChallengeResponseRequest'
ak-stage-authenticator-webauthn: '#/components/schemas/AuthenticatorWebAuthnChallengeResponseRequest'
ak-stage-captcha: '#/components/schemas/CaptchaChallengeResponseRequest'
ak-stage-consent: '#/components/schemas/ConsentChallengeResponseRequest'
ak-stage-dummy: '#/components/schemas/DummyChallengeResponseRequest'
ak-stage-email: '#/components/schemas/EmailChallengeResponseRequest'
ak-stage-identification: '#/components/schemas/IdentificationChallengeResponseRequest'
ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
ak-stage-prompt: '#/components/schemas/PromptResponseChallengeRequest'
ClientTypeEnum: ClientTypeEnum:
enum: enum:
- confidential - confidential
@ -16796,6 +16792,39 @@ components:
- slug - slug
- stages - stages
- title - title
FlowChallengeResponseRequest:
oneOf:
- $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorValidationChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorWebAuthnChallengeResponseRequest'
- $ref: '#/components/schemas/AutoSubmitChallengeResponseRequest'
- $ref: '#/components/schemas/CaptchaChallengeResponseRequest'
- $ref: '#/components/schemas/ConsentChallengeResponseRequest'
- $ref: '#/components/schemas/DummyChallengeResponseRequest'
- $ref: '#/components/schemas/EmailChallengeResponseRequest'
- $ref: '#/components/schemas/IdentificationChallengeResponseRequest'
- $ref: '#/components/schemas/PasswordChallengeResponseRequest'
- $ref: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
- $ref: '#/components/schemas/PromptResponseChallengeRequest'
discriminator:
propertyName: component
mapping:
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
ak-stage-authenticator-totp: '#/components/schemas/AuthenticatorTOTPChallengeResponseRequest'
ak-stage-authenticator-validate: '#/components/schemas/AuthenticatorValidationChallengeResponseRequest'
ak-stage-authenticator-webauthn: '#/components/schemas/AuthenticatorWebAuthnChallengeResponseRequest'
ak-stage-autosubmit: '#/components/schemas/AutoSubmitChallengeResponseRequest'
ak-stage-captcha: '#/components/schemas/CaptchaChallengeResponseRequest'
ak-stage-consent: '#/components/schemas/ConsentChallengeResponseRequest'
ak-stage-dummy: '#/components/schemas/DummyChallengeResponseRequest'
ak-stage-email: '#/components/schemas/EmailChallengeResponseRequest'
ak-stage-identification: '#/components/schemas/IdentificationChallengeResponseRequest'
ak-stage-password: '#/components/schemas/PasswordChallengeResponseRequest'
ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallengeResponseRequest'
ak-stage-prompt: '#/components/schemas/PromptResponseChallengeRequest'
FlowDesignationEnum: FlowDesignationEnum:
enum: enum:
- authentication - authentication
@ -22612,6 +22641,13 @@ components:
- client_id - client_id
- slug - slug
- type - type
PlexAuthenticationChallengeResponseRequest:
type: object
description: Pseudo class for plex response
properties:
component:
type: string
default: ak-flow-sources-plex
PlexSource: PlexSource:
type: object type: object
description: Plex Source Serializer description: Plex Source Serializer

View file

@ -136,13 +136,13 @@ class SeleniumTestCase(StaticLiveServerTestCase):
) )
identification_stage.find_element( identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]" By.CSS_SELECTOR, "input[name=uidField]"
).click() ).click()
identification_stage.find_element( identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]" By.CSS_SELECTOR, "input[name=uidField]"
).send_keys(USER().username) ).send_keys(USER().username)
identification_stage.find_element( identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]" By.CSS_SELECTOR, "input[name=uidField]"
).send_keys(Keys.ENTER) ).send_keys(Keys.ENTER)
flow_executor = self.get_shadow_root("ak-flow-executor") flow_executor = self.get_shadow_root("ak-flow-executor")

View file

@ -40,7 +40,7 @@ export class AutosubmitStage extends BaseStage {
<div class="pf-c-login__main-body"> <div class="pf-c-login__main-body">
<form class="pf-c-form" action="${this.challenge.url}" method="POST"> <form class="pf-c-form" action="${this.challenge.url}" method="POST">
${Object.entries(this.challenge.attrs).map(([ key, value ]) => { ${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
return html`<input type="hidden" .name="${key}" .value="${value}">`; return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
})} })}
<ak-empty-state <ak-empty-state
?loading="${true}"> ?loading="${true}">

View file

@ -10,19 +10,16 @@ export interface StageHost {
export class BaseStage extends LitElement { export class BaseStage extends LitElement {
host?: StageHost; host?: StageHost;
challenge?: Challenge; challenge!: Challenge;
submitForm(e: Event): void { submitForm(e: Event): void {
e.preventDefault(); e.preventDefault();
const object: { const object: {
component: string;
[key: string]: unknown; [key: string]: unknown;
} = { } = {};
component: this.challenge.component,
};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined); const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
form.forEach((value, key) => object[key] = value); form.forEach((value, key) => object[key] = value);
this.host?.submit(object); this.host?.submit(object as unknown as ChallengeResponseRequest);
} }
} }

View file

@ -57,7 +57,7 @@ export class IdentificationStage extends BaseStage {
username.setAttribute("autocomplete", "username"); username.setAttribute("autocomplete", "username");
username.onkeyup = (ev: Event) => { username.onkeyup = (ev: Event) => {
const el = ev.target as HTMLInputElement; const el = ev.target as HTMLInputElement;
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uid_field]").forEach(input => { (this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
input.value = el.value; input.value = el.value;
// Because we assume only one input field exists that matches this // Because we assume only one input field exists that matches this
// call focus so the user can press enter // call focus so the user can press enter
@ -80,7 +80,7 @@ export class IdentificationStage extends BaseStage {
PasswordManagerPrefill.password = el.value; PasswordManagerPrefill.password = el.value;
// Because password managers fill username, then password, // Because password managers fill username, then password,
// we need to re-focus the uid_field here too // we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uid_field]").forEach(input => { (this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this // Because we assume only one input field exists that matches this
// call focus so the user can press enter // call focus so the user can press enter
input.focus(); input.focus();
@ -102,7 +102,7 @@ export class IdentificationStage extends BaseStage {
PasswordManagerPrefill.totp = el.value; PasswordManagerPrefill.totp = el.value;
// Because totp managers fill username, then password, then optionally, // Because totp managers fill username, then password, then optionally,
// we need to re-focus the uid_field here too // we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uid_field]").forEach(input => { (this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this // Because we assume only one input field exists that matches this
// call focus so the user can press enter // call focus so the user can press enter
input.focus(); input.focus();

View file

@ -14,7 +14,7 @@ import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState"; import "../../../elements/EmptyState";
import "../../../elements/Divider"; import "../../../elements/Divider";
import { Error } from "../../../api/Flows"; import { Error } from "../../../api/Flows";
import { Prompt, PromptChallenge } from "authentik-api"; import { Prompt, PromptChallenge, StagePrompt } from "authentik-api";
@customElement("ak-stage-prompt") @customElement("ak-stage-prompt")
@ -27,7 +27,7 @@ export class PromptStage extends BaseStage {
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal]; return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
} }
renderPromptInner(prompt: Prompt): string { renderPromptInner(prompt: StagePrompt): string {
switch (prompt.type) { switch (prompt.type) {
case "text": case "text":
return `<input return `<input