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"""
stage: Optional["StageView"]
component = CharField(default="")
component = CharField(default="xak-flow-response-default")
def __init__(self, instance=None, data=None, **kwargs):
self.stage = kwargs.pop("stage", None)

View file

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

View file

@ -37,12 +37,20 @@ class AutosubmitChallenge(Challenge):
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
class SAMLFlowFinalView(ChallengeStageView):
"""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
(if POST is configured)."""
response_class = AutoSubmitChallengeResponse
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
application: Application = self.executor.plan.context[PLAN_CONTEXT_APPLICATION]
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.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
@ -20,6 +20,12 @@ class PlexAuthenticationChallenge(Challenge):
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):
"""Authenticate against plex.tv"""

View file

@ -28,9 +28,17 @@ class AuthenticatorDuoChallenge(WithUserInfoChallenge):
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):
"""Duo stage"""
response_class = AuthenticatorDuoChallengeResponse
def get_challenge(self, *args, **kwargs) -> Challenge:
user = self.get_pending_user()
stage: AuthenticatorDuoStage = self.executor.current_stage

View file

@ -25,9 +25,17 @@ class AuthenticatorStaticChallenge(WithUserInfoChallenge):
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):
"""Static OTP Setup stage"""
response_class = AuthenticatorStaticChallengeResponse
def get_challenge(self, *args, **kwargs) -> AuthenticatorStaticChallenge:
tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS]
return AuthenticatorStaticChallenge(

View file

@ -3550,13 +3550,13 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ChallengeResponseRequest'
$ref: '#/components/schemas/FlowChallengeResponseRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/ChallengeResponseRequest'
$ref: '#/components/schemas/FlowChallengeResponseRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/ChallengeResponseRequest'
$ref: '#/components/schemas/FlowChallengeResponseRequest'
security:
- authentik: []
- cookieAuth: []
@ -15194,6 +15194,13 @@ components:
- pending_user_avatar
- stage_uuid
- type
AuthenticatorDuoChallengeResponseRequest:
type: object
description: Pseudo class for duo response
properties:
component:
type: string
default: ak-stage-authenticator-duo
AuthenticatorDuoStage:
type: object
description: AuthenticatorDuoStage Serializer
@ -15296,6 +15303,13 @@ components:
- pending_user
- pending_user_avatar
- type
AuthenticatorStaticChallengeResponseRequest:
type: object
description: Pseudo class for static response
properties:
component:
type: string
default: ak-stage-authenticator-static
AuthenticatorStaticStage:
type: object
description: AuthenticatorStaticStage Serializer
@ -15624,6 +15638,13 @@ components:
additionalProperties: {}
required:
- response
AutoSubmitChallengeResponseRequest:
type: object
description: Pseudo class for autosubmit response
properties:
component:
type: string
default: ak-stage-autosubmit
AutosubmitChallenge:
type: object
description: Autosubmit challenge used to send and navigate a POST request
@ -15889,31 +15910,6 @@ components:
- shell
- redirect
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:
enum:
- confidential
@ -16796,6 +16792,39 @@ components:
- slug
- stages
- 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:
enum:
- authentication
@ -22612,6 +22641,13 @@ components:
- client_id
- slug
- type
PlexAuthenticationChallengeResponseRequest:
type: object
description: Pseudo class for plex response
properties:
component:
type: string
default: ak-flow-sources-plex
PlexSource:
type: object
description: Plex Source Serializer

View file

@ -136,13 +136,13 @@ class SeleniumTestCase(StaticLiveServerTestCase):
)
identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]"
By.CSS_SELECTOR, "input[name=uidField]"
).click()
identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]"
By.CSS_SELECTOR, "input[name=uidField]"
).send_keys(USER().username)
identification_stage.find_element(
By.CSS_SELECTOR, "input[name=uid_field]"
By.CSS_SELECTOR, "input[name=uidField]"
).send_keys(Keys.ENTER)
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">
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
${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
?loading="${true}">

View file

@ -10,19 +10,16 @@ export interface StageHost {
export class BaseStage extends LitElement {
host?: StageHost;
challenge?: Challenge;
challenge!: Challenge;
submitForm(e: Event): void {
e.preventDefault();
const object: {
component: string;
[key: string]: unknown;
} = {
component: this.challenge.component,
};
} = {};
const form = new FormData(this.shadowRoot?.querySelector("form") || undefined);
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.onkeyup = (ev: Event) => {
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;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
@ -80,7 +80,7 @@ export class IdentificationStage extends BaseStage {
PasswordManagerPrefill.password = el.value;
// Because password managers fill username, then password,
// 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
// call focus so the user can press enter
input.focus();
@ -102,7 +102,7 @@ export class IdentificationStage extends BaseStage {
PasswordManagerPrefill.totp = el.value;
// Because totp managers fill username, then password, then optionally,
// 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
// call focus so the user can press enter
input.focus();

View file

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