From 6df89e7abfc9eeb76e497a2a3155007941bd7e45 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sun, 21 Feb 2021 19:34:49 +0100 Subject: [PATCH] stages/authenticator_static: migrate to SPA --- .../stages/authenticator_static/forms.py | 22 ----- .../stages/authenticator_static/stage.py | 38 +++++---- .../AuthenticatorStaticStage.ts | 80 +++++++++++++++++++ .../AuthenticatorTOTPStage.ts | 2 +- web/src/pages/generic/FlowExecutor.ts | 4 + 5 files changed, 109 insertions(+), 37 deletions(-) create mode 100644 web/src/elements/stages/authenticator_static/AuthenticatorStaticStage.ts diff --git a/authentik/stages/authenticator_static/forms.py b/authentik/stages/authenticator_static/forms.py index 9195c84fd..95e6b3447 100644 --- a/authentik/stages/authenticator_static/forms.py +++ b/authentik/stages/authenticator_static/forms.py @@ -1,31 +1,9 @@ """Static Authenticator forms""" from django import forms -from django.utils.safestring import mark_safe from authentik.stages.authenticator_static.models import AuthenticatorStaticStage -class StaticTokenWidget(forms.widgets.Widget): - """Widget to render tokens as multiple labels""" - - def render(self, name, value, attrs=None, renderer=None): - final_string = '" - return mark_safe(final_string) # nosec - - -class SetupForm(forms.Form): - """Form to setup Static OTP""" - - tokens = forms.CharField(widget=StaticTokenWidget, disabled=True, required=False) - - def __init__(self, tokens, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["tokens"].initial = tokens - - class AuthenticatorStaticStageForm(forms.ModelForm): """Static Authenticator Stage setup form""" diff --git a/authentik/stages/authenticator_static/stage.py b/authentik/stages/authenticator_static/stage.py index 839ad8afb..697653702 100644 --- a/authentik/stages/authenticator_static/stage.py +++ b/authentik/stages/authenticator_static/stage.py @@ -1,14 +1,16 @@ """Static OTP Setup stage""" -from typing import Any - from django.http import HttpRequest, HttpResponse -from django.views.generic import FormView from django_otp.plugins.otp_static.models import StaticDevice, StaticToken +from rest_framework.fields import CharField, IntegerField, ListField from structlog.stdlib import get_logger +from authentik.flows.challenge import ( + ChallengeResponse, + ChallengeTypes, + WithUserInfoChallenge, +) from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER -from authentik.flows.stage import StageView -from authentik.stages.authenticator_static.forms import SetupForm +from authentik.flows.stage import ChallengeStageView from authentik.stages.authenticator_static.models import AuthenticatorStaticStage LOGGER = get_logger() @@ -16,16 +18,24 @@ SESSION_STATIC_DEVICE = "static_device" SESSION_STATIC_TOKENS = "static_device_tokens" -class AuthenticatorStaticStageView(FormView, StageView): +class AuthenticatorStaticChallenge(WithUserInfoChallenge): + """Static authenticator challenge""" + + codes = ListField(child=CharField()) + + +class AuthenticatorStaticStageView(ChallengeStageView): """Static OTP Setup stage""" - form_class = SetupForm - - def get_form_kwargs(self, **kwargs) -> dict[str, Any]: - kwargs = super().get_form_kwargs(**kwargs) - tokens = self.request.session[SESSION_STATIC_TOKENS] - kwargs["tokens"] = tokens - return kwargs + def get_challenge(self, *args, **kwargs) -> AuthenticatorStaticChallenge: + tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS] + return AuthenticatorStaticChallenge( + data={ + "type": ChallengeTypes.native, + "component": "ak-stage-authenticator-static", + "codes": [token.token for token in tokens], + } + ) def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: user = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) @@ -51,7 +61,7 @@ class AuthenticatorStaticStageView(FormView, StageView): self.request.session[SESSION_STATIC_TOKENS] = tokens return super().get(request, *args, **kwargs) - def form_valid(self, form: SetupForm) -> HttpResponse: + def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: """Verify OTP Token""" device: StaticDevice = self.request.session[SESSION_STATIC_DEVICE] device.save() diff --git a/web/src/elements/stages/authenticator_static/AuthenticatorStaticStage.ts b/web/src/elements/stages/authenticator_static/AuthenticatorStaticStage.ts new file mode 100644 index 000000000..d22e3ae36 --- /dev/null +++ b/web/src/elements/stages/authenticator_static/AuthenticatorStaticStage.ts @@ -0,0 +1,80 @@ +import { gettext } from "django"; +import { css, CSSResult, customElement, html, property, TemplateResult } from "lit-element"; +import { WithUserInfoChallenge } from "../../../api/Flows"; +import { COMMON_STYLES } from "../../../common/styles"; +import { BaseStage } from "../base"; + +export interface AuthenticatorStaticChallenge extends WithUserInfoChallenge { + codes: number[]; +} + +@customElement("ak-stage-authenticator-static") +export class AuthenticatorStaticStage extends BaseStage { + + @property({ attribute: false }) + challenge?: AuthenticatorStaticChallenge; + + static get styles(): CSSResult[] { + return COMMON_STYLES.concat(css` + /* Static OTP Tokens */ + .ak-otp-tokens { + list-style: circle; + columns: 2; + -webkit-columns: 2; + -moz-columns: 2; + margin-left: var(--pf-global--spacer--xs); + } + .ak-otp-tokens li { + font-size: var(--pf-global--FontSize--2xl); + font-family: monospace; + } + `); + } + + render(): TemplateResult { + if (!this.challenge) { + return html``; + } + return html`
+

+ ${this.challenge.title} +

+
+
+
{ this.submit(e); }}> +
+
+
+ ${gettext( + ${this.challenge.pending_user} +
+ +
+
+ +
    + ${this.challenge.codes.map((token) => { + return html`
  • ${token}
  • `; + })} +
+
+ +
+ +
+
+
+ `; + } + +} diff --git a/web/src/elements/stages/authenticator_totp/AuthenticatorTOTPStage.ts b/web/src/elements/stages/authenticator_totp/AuthenticatorTOTPStage.ts index a0719f568..d999dbfa9 100644 --- a/web/src/elements/stages/authenticator_totp/AuthenticatorTOTPStage.ts +++ b/web/src/elements/stages/authenticator_totp/AuthenticatorTOTPStage.ts @@ -3,7 +3,7 @@ import { CSSResult, customElement, html, property, TemplateResult } from "lit-el import { WithUserInfoChallenge } from "../../../api/Flows"; import { COMMON_STYLES } from "../../../common/styles"; import { BaseStage } from "../base"; -import 'webcomponent-qr-code' +import "webcomponent-qr-code"; export interface AuthenticatorTOTPChallenge extends WithUserInfoChallenge { config_url: string; diff --git a/web/src/pages/generic/FlowExecutor.ts b/web/src/pages/generic/FlowExecutor.ts index ddcad57f3..f3e58a3f6 100644 --- a/web/src/pages/generic/FlowExecutor.ts +++ b/web/src/pages/generic/FlowExecutor.ts @@ -9,6 +9,7 @@ import "../../elements/stages/email/EmailStage"; import "../../elements/stages/autosubmit/AutosubmitStage"; import "../../elements/stages/prompt/PromptStage"; import "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage"; +import "../../elements/stages/authenticator_static/AuthenticatorStaticStage"; import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows"; import { DefaultClient } from "../../api/Client"; import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage"; @@ -18,6 +19,7 @@ import { EmailChallenge } from "../../elements/stages/email/EmailStage"; import { AutosubmitChallenge } from "../../elements/stages/autosubmit/AutosubmitStage"; import { PromptChallenge } from "../../elements/stages/prompt/PromptStage"; import { AuthenticatorTOTPChallenge } from "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage"; +import { AuthenticatorStaticChallenge } from "../../elements/stages/authenticator_static/AuthenticatorStaticStage"; @customElement("ak-flow-executor") export class FlowExecutor extends LitElement { @@ -128,6 +130,8 @@ export class FlowExecutor extends LitElement { return html``; case "ak-stage-authenticator-totp": return html``; + case "ak-stage-authenticator-static": + return html``; default: break; }