flows: add default challenge response
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
fb4e0723ee
commit
c6bb6709fd
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
),
|
),
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
92
schema.yml
92
schema.yml
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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}">
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
Reference in a new issue