From 3894895d32f0ad9b97be67ab5a98e441a2effbfe Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 23 Feb 2021 13:50:47 +0100 Subject: [PATCH] stages/authenticator_validate: start rewrite to SPA --- authentik/flows/models.py | 1 + .../stages/authenticator_validate/models.py | 1 + .../stages/authenticator_validate/stage.py | 79 ++++++++------- .../stages/authenticator_validate/webauthn.py | 83 ++++++++++++++++ .../stages/authenticator_webauthn/stage.py | 2 +- .../stages/authenticator_webauthn/urls.py | 12 --- .../stages/authenticator_webauthn/views.py | 99 ------------------- tests/e2e/test_flows_authenticators.py | 43 ++++++-- .../AuthenticatorValidateStage.ts | 43 +++++++- .../AuthenticatorValidateStageWebAuthn.ts} | 43 +++----- .../WebAuthnAuthenticatorRegisterStage.ts | 4 +- .../stages/authenticator_webauthn/utils.ts | 45 --------- web/src/elements/stages/base.ts | 10 +- web/src/pages/generic/FlowExecutor.ts | 7 +- 14 files changed, 236 insertions(+), 236 deletions(-) create mode 100644 authentik/stages/authenticator_validate/webauthn.py rename web/src/elements/stages/{authenticator_webauthn/WebAuthnAuth.ts => authenticator_validate/AuthenticatorValidateStageWebAuthn.ts} (70%) diff --git a/authentik/flows/models.py b/authentik/flows/models.py index 3a0acbe08..45f0f61ba 100644 --- a/authentik/flows/models.py +++ b/authentik/flows/models.py @@ -23,6 +23,7 @@ class NotConfiguredAction(models.TextChoices): """Decides how the FlowExecutor should proceed when a stage isn't configured""" SKIP = "skip" + DENY = "deny" # CONFIGURE = "configure" diff --git a/authentik/stages/authenticator_validate/models.py b/authentik/stages/authenticator_validate/models.py index 688cd0e1a..73d060751 100644 --- a/authentik/stages/authenticator_validate/models.py +++ b/authentik/stages/authenticator_validate/models.py @@ -14,6 +14,7 @@ from authentik.flows.models import NotConfiguredAction, Stage class DeviceClasses(models.TextChoices): """Device classes this stage can validate""" + # device class must match Device's class name so StaticDevice -> static STATIC = "static" TOTP = "totp", _("TOTP") WEBAUTHN = "webauthn", _("WebAuthn") diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index 54d4367a2..01506d738 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -1,38 +1,44 @@ -"""OTP Validation""" +"""Authenticator Validation""" from django.http import HttpRequest, HttpResponse -from django_otp import user_has_device -from rest_framework.fields import IntegerField +from django_otp import devices_for_user, user_has_device +from rest_framework.fields import CharField, DictField, IntegerField, JSONField, ListField from structlog.stdlib import get_logger -from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes +from authentik.flows.challenge import ( + ChallengeResponse, + ChallengeTypes, + WithUserInfoChallenge, +) from authentik.flows.models import NotConfiguredAction from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.stage import ChallengeStageView -from authentik.stages.authenticator_validate.forms import ValidationForm from authentik.stages.authenticator_validate.models import AuthenticatorValidateStage LOGGER = get_logger() -class CodeChallengeResponse(ChallengeResponse): +class AuthenticatorChallenge(WithUserInfoChallenge): + """Authenticator challenge""" + + users_device_classes = ListField(child=CharField()) + class_challenges = DictField(JSONField()) + + +class AuthenticatorChallengeResponse(ChallengeResponse): """Challenge used for Code-based authenticators""" - code = IntegerField(min_value=0) + device_challenges = DictField(JSONField()) - -class WebAuthnChallengeResponse(ChallengeResponse): - """Challenge used for WebAuthn authenticators""" + def validate_device_challenges(self, value: dict[str, dict]): + return value class AuthenticatorValidateStageView(ChallengeStageView): - """OTP Validation""" + """Authenticator Validation""" - form_class = ValidationForm + response_class = AuthenticatorChallengeResponse - # def get_form_kwargs(self, **kwargs) -> dict[str, Any]: - # kwargs = super().get_form_kwargs(**kwargs) - # kwargs["user"] = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) - # return kwargs + allowed_device_classes: set[str] def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """Check if a user is set, and check if the user has any devices @@ -44,33 +50,38 @@ class AuthenticatorValidateStageView(ChallengeStageView): has_devices = user_has_device(user) stage: AuthenticatorValidateStage = self.executor.current_stage - if not has_devices: + user_devices = devices_for_user(self.get_pending_user()) + user_device_classes = set( + [ + device.__class__.__name__.lower().replace("device", "") + for device in user_devices + ] + ) + stage_device_classes = set(self.executor.current_stage.device_classes) + self.allowed_device_classes = user_device_classes.intersection(stage_device_classes) + + # User has no devices, or the devices they have don't overlap with the allowed + # classes + if not has_devices or len(self.allowed_device_classes) < 1: if stage.not_configured_action == NotConfiguredAction.SKIP: LOGGER.debug("Authenticator not configured, skipping stage") return self.executor.stage_ok() + if stage.not_configured_action == NotConfiguredAction.DENY: + LOGGER.debug("Authenticator not configured, denying") + return self.executor.stage_invalid() return super().get(request, *args, **kwargs) - # def get_form_kwargs(self, **kwargs) -> Dict[str, Any]: - # kwargs = super().get_form_kwargs(**kwargs) - # kwargs["user"] = self.executor.plan.context.get(PLAN_CONTEXT_PENDING_USER) - # return kwargs - - def get_challenge(self) -> Challenge: - return Challenge( - { + def get_challenge(self) -> AuthenticatorChallenge: + return AuthenticatorChallenge( + data={ "type": ChallengeTypes.native, - # TODO: use component based on devices "component": "ak-stage-authenticator-validate", - "args": {"user": "foo.bar.baz"}, + "users_device_classes": self.allowed_device_classes, } ) - def challenge_valid(self, challenge: ChallengeResponse) -> HttpResponse: + def challenge_valid( + self, challenge: AuthenticatorChallengeResponse + ) -> HttpResponse: print(challenge) return HttpResponse() - - # def form_valid(self, form: ValidationForm) -> HttpResponse: - # """Verify OTP Token""" - # # Since we do token checking in the form, we know the token is valid here - # # so we can just continue - # return self.executor.stage_ok() diff --git a/authentik/stages/authenticator_validate/webauthn.py b/authentik/stages/authenticator_validate/webauthn.py new file mode 100644 index 000000000..76e24cb96 --- /dev/null +++ b/authentik/stages/authenticator_validate/webauthn.py @@ -0,0 +1,83 @@ +from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser +from webauthn.webauthn import ( + AuthenticationRejectedException, + RegistrationRejectedException, + WebAuthnUserDataMissing, +) + +class BeginAssertion(FlowUserRequiredView): + """Send the client a challenge that we'll check later""" + + def post(self, request: HttpRequest) -> HttpResponse: + """Send the client a challenge that we'll check later""" + request.session.pop("challenge", None) + + challenge = generate_challenge(32) + + # We strip the padding from the challenge stored in the session + # for the reasons outlined in the comment in webauthn_begin_activate. + request.session["challenge"] = challenge.rstrip("=") + + devices = WebAuthnDevice.objects.filter(user=self.user) + if not devices.exists(): + return HttpResponseBadRequest() + device: WebAuthnDevice = devices.first() + + webauthn_user = WebAuthnUser( + self.user.uid, + self.user.username, + self.user.name, + avatar(self.user), + device.credential_id, + device.public_key, + device.sign_count, + device.rp_id, + ) + + webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) + + return JsonResponse(webauthn_assertion_options.assertion_dict) + + +class VerifyAssertion(FlowUserRequiredView): + """Verify assertion result that we've sent to the client""" + + def post(self, request: HttpRequest) -> HttpResponse: + """Verify assertion result that we've sent to the client""" + challenge = request.session.get("challenge") + assertion_response = request.POST + credential_id = assertion_response.get("id") + + device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() + if not device: + return JsonResponse({"fail": "Device does not exist."}, status=401) + + webauthn_user = WebAuthnUser( + self.user.uid, + self.user.username, + self.user.name, + avatar(self.user), + device.credential_id, + device.public_key, + device.sign_count, + device.rp_id, + ) + + webauthn_assertion_response = WebAuthnAssertionResponse( + webauthn_user, assertion_response, challenge, ORIGIN, uv_required=False + ) # User Verification + + try: + sign_count = webauthn_assertion_response.verify() + except ( + AuthenticationRejectedException, + WebAuthnUserDataMissing, + RegistrationRejectedException, + ) as exc: + return JsonResponse({"fail": "Assertion failed. Error: {}".format(exc)}) + + device.set_sign_count(sign_count) + request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True + return JsonResponse( + {"success": "Successfully authenticated as {}".format(self.user.username)} + ) diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py index bf25a4628..5bc5cf37a 100644 --- a/authentik/stages/authenticator_webauthn/stage.py +++ b/authentik/stages/authenticator_webauthn/stage.py @@ -122,7 +122,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): return AuthenticatorWebAuthnChallenge( data={ "type": ChallengeTypes.native, - "component": "ak-stage-authenticator-webauthn-register", + "component": "ak-stage-authenticator-webauthn", "registration": make_credential_options.registration_dict, } ) diff --git a/authentik/stages/authenticator_webauthn/urls.py b/authentik/stages/authenticator_webauthn/urls.py index aadba3c76..d5236a829 100644 --- a/authentik/stages/authenticator_webauthn/urls.py +++ b/authentik/stages/authenticator_webauthn/urls.py @@ -3,22 +3,10 @@ from django.urls import path from django.views.decorators.csrf import csrf_exempt from authentik.stages.authenticator_webauthn.views import ( - BeginAssertion, UserSettingsView, - VerifyAssertion, ) urlpatterns = [ - path( - "begin-assertion/", - csrf_exempt(BeginAssertion.as_view()), - name="assertion-begin", - ), - path( - "verify-assertion/", - csrf_exempt(VerifyAssertion.as_view()), - name="assertion-verify", - ), path( "/settings/", UserSettingsView.as_view(), name="user-settings" ), diff --git a/authentik/stages/authenticator_webauthn/views.py b/authentik/stages/authenticator_webauthn/views.py index 5ff9da611..65374616d 100644 --- a/authentik/stages/authenticator_webauthn/views.py +++ b/authentik/stages/authenticator_webauthn/views.py @@ -6,12 +6,6 @@ from django.shortcuts import get_object_or_404 from django.views import View from django.views.generic import TemplateView from structlog.stdlib import get_logger -from webauthn import WebAuthnAssertionOptions, WebAuthnAssertionResponse, WebAuthnUser -from webauthn.webauthn import ( - AuthenticationRejectedException, - RegistrationRejectedException, - WebAuthnUserDataMissing, -) from authentik.core.models import User from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER @@ -32,99 +26,6 @@ RP_NAME = "authentik" ORIGIN = "http://localhost:8000" -class FlowUserRequiredView(View): - """Base class for views which can only be called in the context of a flow.""" - - user: User - - def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - plan = request.session.get(SESSION_KEY_PLAN, None) - if not plan: - return HttpResponseBadRequest() - self.user = plan.context.get(PLAN_CONTEXT_PENDING_USER) - if not self.user: - return HttpResponseBadRequest() - return super().dispatch(request, *args, **kwargs) - - -class BeginAssertion(FlowUserRequiredView): - """Send the client a challenge that we'll check later""" - - def post(self, request: HttpRequest) -> HttpResponse: - """Send the client a challenge that we'll check later""" - request.session.pop("challenge", None) - - challenge = generate_challenge(32) - - # We strip the padding from the challenge stored in the session - # for the reasons outlined in the comment in webauthn_begin_activate. - request.session["challenge"] = challenge.rstrip("=") - - devices = WebAuthnDevice.objects.filter(user=self.user) - if not devices.exists(): - return HttpResponseBadRequest() - device: WebAuthnDevice = devices.first() - - webauthn_user = WebAuthnUser( - self.user.uid, - self.user.username, - self.user.name, - avatar(self.user), - device.credential_id, - device.public_key, - device.sign_count, - device.rp_id, - ) - - webauthn_assertion_options = WebAuthnAssertionOptions(webauthn_user, challenge) - - return JsonResponse(webauthn_assertion_options.assertion_dict) - - -class VerifyAssertion(FlowUserRequiredView): - """Verify assertion result that we've sent to the client""" - - def post(self, request: HttpRequest) -> HttpResponse: - """Verify assertion result that we've sent to the client""" - challenge = request.session.get("challenge") - assertion_response = request.POST - credential_id = assertion_response.get("id") - - device = WebAuthnDevice.objects.filter(credential_id=credential_id).first() - if not device: - return JsonResponse({"fail": "Device does not exist."}, status=401) - - webauthn_user = WebAuthnUser( - self.user.uid, - self.user.username, - self.user.name, - avatar(self.user), - device.credential_id, - device.public_key, - device.sign_count, - device.rp_id, - ) - - webauthn_assertion_response = WebAuthnAssertionResponse( - webauthn_user, assertion_response, challenge, ORIGIN, uv_required=False - ) # User Verification - - try: - sign_count = webauthn_assertion_response.verify() - except ( - AuthenticationRejectedException, - WebAuthnUserDataMissing, - RegistrationRejectedException, - ) as exc: - return JsonResponse({"fail": "Assertion failed. Error: {}".format(exc)}) - - device.set_sign_count(sign_count) - request.session[SESSION_KEY_WEBAUTHN_AUTHENTICATED] = True - return JsonResponse( - {"success": "Successfully authenticated as {}".format(self.user.username)} - ) - - class UserSettingsView(LoginRequiredMixin, TemplateView): """View for user settings to control WebAuthn devices""" diff --git a/tests/e2e/test_flows_authenticators.py b/tests/e2e/test_flows_authenticators.py index 632454932..b72821a23 100644 --- a/tests/e2e/test_flows_authenticators.py +++ b/tests/e2e/test_flows_authenticators.py @@ -37,16 +37,45 @@ class TestFlowsAuthenticator(SeleniumTestCase): ) self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/") - self.driver.find_element(By.ID, "id_uid_field").click() - self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username) - self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER) - self.driver.find_element(By.ID, "id_password").send_keys(USER().username) - self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER) + + flow_executor = self.get_shadow_root("ak-flow-executor") + identification_stage = self.get_shadow_root( + "ak-stage-identification", flow_executor + ) + + identification_stage.find_element( + By.CSS_SELECTOR, "input[name=uid_field]" + ).click() + identification_stage.find_element( + By.CSS_SELECTOR, "input[name=uid_field]" + ).send_keys(USER().username) + identification_stage.find_element( + By.CSS_SELECTOR, "input[name=uid_field]" + ).send_keys(Keys.ENTER) + + flow_executor = self.get_shadow_root("ak-flow-executor") + password_stage = self.get_shadow_root("ak-stage-password", flow_executor) + password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys( + USER().username + ) + password_stage.find_element(By.CSS_SELECTOR, "input[name=password]").send_keys( + Keys.ENTER + ) # Get expected token totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift) - self.driver.find_element(By.ID, "id_code").send_keys(totp.token()) - self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER) + + flow_executor = self.get_shadow_root("ak-flow-executor") + identification_stage = self.get_shadow_root( + "ak-stage-identification", flow_executor + ) + + self.driver.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys( + totp.token() + ) + self.driver.find_element(By.CSS_SELECTOR, "input[name=code]").send_keys( + Keys.ENTER + ) self.wait_for_url(self.shell_url("authentik_core:overview")) self.assert_user(USER()) diff --git a/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts b/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts index ce9ba9a82..83bafc866 100644 --- a/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts +++ b/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStage.ts @@ -1,9 +1,48 @@ -import { customElement, html, LitElement, TemplateResult } from "lit-element"; +import { customElement, html, property, TemplateResult } from "lit-element"; +import { WithUserInfoChallenge } from "../../../api/Flows"; +import { BaseStage, StageHost } from "../base"; +import "./AuthenticatorValidateStageWebAuthn"; + +export enum DeviceClasses { + STATIC = "static", + TOTP = "totp", + WEBAUTHN = "webauthn", +} + +export interface AuthenticatorValidateStageChallenge extends WithUserInfoChallenge { + users_device_classes: DeviceClasses[]; + class_challenges: { [key in DeviceClasses]: unknown }; +} + +export interface AuthenticatorValidateStageChallengeResponse { + device_challenges: { [key in DeviceClasses]: unknown} ; +} @customElement("ak-stage-authenticator-validate") -export class AuthenticatorValidateStage extends LitElement { +export class AuthenticatorValidateStage extends BaseStage implements StageHost { + + @property({ attribute: false }) + challenge?: AuthenticatorValidateStageChallenge; + + renderDeviceClass(deviceClass: DeviceClasses): TemplateResult { + switch (deviceClass) { + case DeviceClasses.STATIC: + case DeviceClasses.TOTP: + return html``; + case DeviceClasses.WEBAUTHN: + return html``; + } + } + + submit(formData?: FormData): Promise { + return this.host?.submit(formData) || Promise.resolve(); + } render(): TemplateResult { + // User only has a single device class, so we don't show a picker + if (this.challenge?.users_device_classes.length === 1) { + return this.renderDeviceClass(this.challenge.users_device_classes[0]); + } return html`ak-stage-authenticator-validate`; } diff --git a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuth.ts b/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn.ts similarity index 70% rename from web/src/elements/stages/authenticator_webauthn/WebAuthnAuth.ts rename to web/src/elements/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn.ts index ce3266947..cb6d2519f 100644 --- a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuth.ts +++ b/web/src/elements/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn.ts @@ -1,10 +1,15 @@ import { gettext } from "django"; -import { customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { customElement, html, property, TemplateResult } from "lit-element"; import { SpinnerSize } from "../../Spinner"; -import { getCredentialRequestOptionsFromServer, postAssertionToServer, transformAssertionForServer, transformCredentialRequestOptions } from "./utils"; +import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils"; +import { BaseStage } from "../base"; +import { AuthenticatorValidateStageChallenge, DeviceClasses } from "./AuthenticatorValidateStage"; -@customElement("ak-stage-webauthn-auth") -export class WebAuthnAuth extends LitElement { +@customElement("ak-stage-authenticator-validate-webauthn") +export class AuthenticatorValidateStageWebAuthn extends BaseStage { + + @property({attribute: false}) + challenge?: AuthenticatorValidateStageChallenge; @property({ type: Boolean }) authenticateRunning = false; @@ -13,18 +18,10 @@ export class WebAuthnAuth extends LitElement { authenticateMessage = ""; async authenticate(): Promise { - // post the login data to the server to retrieve the PublicKeyCredentialRequestOptions - let credentialRequestOptionsFromServer; - try { - credentialRequestOptionsFromServer = await getCredentialRequestOptionsFromServer(); - } catch (err) { - throw new Error(gettext(`Error when getting request options from server: ${err}`)); - } - // convert certain members of the PublicKeyCredentialRequestOptions into // byte arrays as expected by the spec. - const transformedCredentialRequestOptions = transformCredentialRequestOptions( - credentialRequestOptionsFromServer); + const credentialRequestOptions = this.challenge?.class_challenges[DeviceClasses.WEBAUTHN]; + const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions); // request the authenticator to create an assertion signature using the // credential private key @@ -42,26 +39,16 @@ export class WebAuthnAuth extends LitElement { // we now have an authentication assertion! encode the byte arrays contained // in the assertion data as strings for posting to the server - const transformedAssertionForServer = transformAssertionForServer(assertion); + const transformedAssertionForServer = transformAssertionForServer(assertion); // post the assertion to the server for verification. try { - await postAssertionToServer(transformedAssertionForServer); + const formData = new FormData(); + formData.set(`response[${DeviceClasses.WEBAUTHN}]`, JSON.stringify(transformedAssertionForServer)); + await this.host?.submit(formData); } catch (err) { throw new Error(gettext(`Error when validating assertion on server: ${err}`)); } - - this.finishStage(); - } - - finishStage(): void { - // Mark this stage as done - this.dispatchEvent( - new CustomEvent("ak-flow-submit", { - bubbles: true, - composed: true, - }) - ); } firstUpdated(): void { diff --git a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts b/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts index b9981db2b..70e355466 100644 --- a/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts +++ b/web/src/elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage.ts @@ -13,7 +13,7 @@ export interface WebAuthnAuthenticatorRegisterChallengeResponse { response: Assertion; } -@customElement("ak-stage-authenticator-webauthn-register") +@customElement("ak-stage-authenticator-webauthn") export class WebAuthnAuthenticatorRegisterStage extends BaseStage { @property({ attribute: false }) @@ -58,7 +58,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage { // and storing the public key try { const formData = new FormData(); - formData.set("response", JSON.stringify(newAssertionForServer)) + formData.set("response", JSON.stringify(newAssertionForServer)); await this.host?.submit(formData); } catch (err) { throw new Error(gettext(`Server validation of credential failed: ${err}`)); diff --git a/web/src/elements/stages/authenticator_webauthn/utils.ts b/web/src/elements/stages/authenticator_webauthn/utils.ts index c614df4d7..e6c064a6d 100644 --- a/web/src/elements/stages/authenticator_webauthn/utils.ts +++ b/web/src/elements/stages/authenticator_webauthn/utils.ts @@ -21,20 +21,6 @@ export function hexEncode(buf: Uint8Array): string { .join(""); } -export interface GenericResponse { - fail?: string; - success?: string; - [key: string]: string | number | GenericResponse | undefined; -} - -async function fetchJSON(url: string, options: RequestInit): Promise { - const response = await fetch(url, options); - const body = await response.json(); - if (body.fail) - throw body.fail; - return body; -} - /** * Transforms items in the credentialCreateOptions generated on the server * into byte arrays expected by the navigator.credentials.create() call @@ -84,20 +70,6 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential }; } -/** - * Get PublicKeyCredentialRequestOptions for this user from the server - * formData of the registration form - * @param {FormData} formData - */ -export async function getCredentialRequestOptionsFromServer(): Promise { - return await fetchJSON( - "/-/user/authenticator/webauthn/begin-assertion/", - { - method: "POST", - } - ); -} - function u8arr(input: string): Uint8Array { return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0)); } @@ -150,20 +122,3 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential): assertionClientExtensions: JSON.stringify(assertionClientExtensions) }; } - -/** - * Post the assertion to the server for validation and logging the user in. - * @param {Object} assertionDataForServer - */ -export async function postAssertionToServer(assertionDataForServer: Assertion): Promise { - const formData = new FormData(); - Object.entries(assertionDataForServer).forEach(([key, value]) => { - formData.set(key, value); - }); - - return await fetchJSON( - "/-/user/authenticator/webauthn/verify-assertion/", { - method: "POST", - body: formData - }); -} diff --git a/web/src/elements/stages/base.ts b/web/src/elements/stages/base.ts index fe7ec20e0..1e9b1db54 100644 --- a/web/src/elements/stages/base.ts +++ b/web/src/elements/stages/base.ts @@ -1,13 +1,17 @@ import { LitElement } from "lit-element"; -import { FlowExecutor } from "../../pages/generic/FlowExecutor"; + +export interface StageHost { + submit(formData?: FormData): Promise; +} export class BaseStage extends LitElement { - host?: FlowExecutor; + host?: StageHost; - submit(e: Event): void { + submitForm(e: Event): void { e.preventDefault(); const form = new FormData(this.shadowRoot?.querySelector("form") || undefined); this.host?.submit(form); } + } diff --git a/web/src/pages/generic/FlowExecutor.ts b/web/src/pages/generic/FlowExecutor.ts index 25945c6d2..8e73546dd 100644 --- a/web/src/pages/generic/FlowExecutor.ts +++ b/web/src/pages/generic/FlowExecutor.ts @@ -24,9 +24,10 @@ import { AuthenticatorStaticChallenge } from "../../elements/stages/authenticato import { WebAuthnAuthenticatorRegisterChallenge } from "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; import { COMMON_STYLES } from "../../common/styles"; import { SpinnerSize } from "../../elements/Spinner"; +import { StageHost } from "../../elements/stages/base"; @customElement("ak-flow-executor") -export class FlowExecutor extends LitElement { +export class FlowExecutor extends LitElement implements StageHost { @property() flowSlug = ""; @@ -158,8 +159,8 @@ export class FlowExecutor extends LitElement { return html``; case "ak-stage-authenticator-static": return html``; - case "ak-stage-authenticator-webauthn-register": - return html``; + case "ak-stage-authenticator-webauthn": + return html``; default: break; }