stages: authenticator_endpoint_gdtc (#10477)

* rework

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add loading overlay for chrome

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

* save data

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix web ui, prevent deletion

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* text fixes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
Jens L.
2024-10-22 22:46:46 +02:00
committed by GitHub
parent 0e4e7ccb4b
commit cec3fdb612
30 changed files with 1803 additions and 31 deletions

View File

@ -2,6 +2,7 @@ import "@goauthentik/admin/rbac/ObjectPermissionModal";
import "@goauthentik/admin/stages/StageWizard";
import "@goauthentik/admin/stages/authenticator_duo/AuthenticatorDuoStageForm";
import "@goauthentik/admin/stages/authenticator_duo/DuoDeviceImportForm";
import "@goauthentik/admin/stages/authenticator_endpoint_gdtc/AuthenticatorEndpointGDTCStageForm";
import "@goauthentik/admin/stages/authenticator_sms/AuthenticatorSMSStageForm";
import "@goauthentik/admin/stages/authenticator_static/AuthenticatorStaticStageForm";
import "@goauthentik/admin/stages/authenticator_totp/AuthenticatorTOTPStageForm";
@ -25,8 +26,7 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/forms/DeleteBulkForm";
import "@goauthentik/elements/forms/ModalForm";
import "@goauthentik/elements/forms/ProxyForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { TableColumn } from "@goauthentik/elements/table/Table";
import { PaginatedResponse, TableColumn } from "@goauthentik/elements/table/Table";
import { TablePage } from "@goauthentik/elements/table/TablePage";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";

View File

@ -0,0 +1,75 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { AuthenticatorEndpointGDTCStage, StagesApi } from "@goauthentik/api";
@customElement("ak-stage-authenticator-endpoint-gdtc-form")
export class AuthenticatorEndpointGDTCStageForm extends BaseStageForm<AuthenticatorEndpointGDTCStage> {
loadInstance(pk: string): Promise<AuthenticatorEndpointGDTCStage> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEndpointGdtcRetrieve({
stageUuid: pk,
});
}
async send(data: AuthenticatorEndpointGDTCStage): Promise<AuthenticatorEndpointGDTCStage> {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEndpointGdtcPartialUpdate({
stageUuid: this.instance.pk || "",
patchedAuthenticatorEndpointGDTCStageRequest: data,
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorEndpointGdtcCreate({
authenticatorEndpointGDTCStageRequest: data,
});
}
}
renderForm(): TemplateResult {
return html` <span>
${msg(
"Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows.",
)}
</span>
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${first(this.instance?.name, "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Google Verified Access API")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Credentials")}
required
name="credentials"
>
<ak-codemirror
mode=${CodeMirrorMode.JavaScript}
.value="${first(this.instance?.credentials, {})}"
></ak-codemirror>
<p class="pf-c-form__helper-text">
${msg("Google Cloud credentials file.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-stage-authenticator-endpoint-gdtc-form": AuthenticatorEndpointGDTCStageForm;
}
}

View File

@ -1,11 +1,12 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SentryIgnoredError } from "@goauthentik/common/errors";
import { deviceTypeName } from "@goauthentik/common/labels";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/elements/forms/DeleteBulkForm";
import { PaginatedResponse } from "@goauthentik/elements/table/Table";
import { Table, TableColumn } from "@goauthentik/elements/table/Table";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
@ -54,20 +55,21 @@ export class UserDeviceTable extends Table<Device> {
async deleteWrapper(device: Device) {
const api = new AuthenticatorsApi(DEFAULT_CONFIG);
const id = { id: device.pk };
switch (device.type) {
case "authentik_stages_authenticator_duo.DuoDevice":
return api.authenticatorsAdminDuoDestroy(id);
return api.authenticatorsAdminDuoDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_sms.SMSDevice":
return api.authenticatorsAdminSmsDestroy(id);
return api.authenticatorsAdminSmsDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_totp.TOTPDevice":
return api.authenticatorsAdminTotpDestroy(id);
return api.authenticatorsAdminTotpDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_static.StaticDevice":
return api.authenticatorsAdminStaticDestroy(id);
return api.authenticatorsAdminStaticDestroy({ id: parseInt(device.pk, 10) });
case "authentik_stages_authenticator_webauthn.WebAuthnDevice":
return api.authenticatorsAdminWebauthnDestroy(id);
return api.authenticatorsAdminWebauthnDestroy({ id: parseInt(device.pk, 10) });
default:
break;
throw new SentryIgnoredError(
msg(str`Device type ${device.verboseName} cannot be deleted`),
);
}
}

View File

@ -16,6 +16,7 @@ import { themeImage } from "@goauthentik/elements/utils/images";
import "@goauthentik/flow/sources/apple/AppleLoginInit";
import "@goauthentik/flow/sources/plex/PlexLoginInit";
import "@goauthentik/flow/stages/FlowErrorStage";
import "@goauthentik/flow/stages/FlowFrameStage";
import "@goauthentik/flow/stages/RedirectStage";
import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
@ -170,6 +171,19 @@ export class FlowExecutor extends Interface implements StageHost {
this.addEventListener(EVENT_FLOW_INSPECTOR_TOGGLE, () => {
this.inspectorOpen = !this.inspectorOpen;
});
window.addEventListener("message", (event) => {
const msg: {
source?: string;
context?: string;
message: string;
} = event.data;
if (msg.source !== "goauthentik.io" || msg.context !== "flow-executor") {
return;
}
if (msg.message === "submit") {
this.submit({} as FlowChallengeResponseRequest);
}
});
}
async getTheme(): Promise<UiThemeEnum> {
@ -429,6 +443,11 @@ export class FlowExecutor extends Interface implements StageHost {
</ak-stage-redirect>`;
case "xak-flow-shell":
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
case "xak-flow-frame":
return html`<xak-flow-frame
.host=${this as StageHost}
.challenge=${this.challenge}
></xak-flow-frame>`;
default:
return html`Invalid native challenge element`;
}

View File

@ -114,7 +114,7 @@ export class InputPassword extends AKElement {
this.input.type = "password";
this.input.name = this.name;
this.input.placeholder = this.placeholder;
this.input.autofocus = true;
this.input.autofocus = this.grabFocus;
this.input.autocomplete = "current-password";
this.input.classList.add("pf-c-form-control");
this.input.required = true;

View File

@ -0,0 +1,54 @@
import "@goauthentik/elements/EmptyState";
import "@goauthentik/flow/FormStatic";
import { BaseStage } from "@goauthentik/flow/stages/base";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { FrameChallenge, FrameChallengeResponseRequest } from "@goauthentik/api";
@customElement("xak-flow-frame")
export class FlowFrameStage extends BaseStage<FrameChallenge, FrameChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, css``];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`;
}
return html` <header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
${this.challenge.loadingOverlay
? html`<ak-empty-state
loading
header=${this.challenge.loadingText ?? undefined}
>
</ak-empty-state>`
: nothing}
<iframe
style=${this.challenge.loadingOverlay
? "width:0;height:0;position:absolute;"
: ""}
src=${this.challenge.url}
></iframe>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"xak-flow-frame": FlowFrameStage;
}
}

View File

@ -1,8 +1,9 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SentryIgnoredError } from "@goauthentik/common/errors";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -10,11 +11,11 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { AuthenticatorsApi, Device } from "@goauthentik/api";
@customElement("ak-user-mfa-form")
export class MFADeviceForm extends ModelForm<Device, number> {
export class MFADeviceForm extends ModelForm<Device, string> {
@property()
deviceType!: string;
async loadInstance(pk: number): Promise<Device> {
async loadInstance(pk: string): Promise<Device> {
const devices = await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsAllList();
return devices.filter((device) => {
return device.pk === pk && device.type === this.deviceType;
@ -29,36 +30,38 @@ export class MFADeviceForm extends ModelForm<Device, number> {
switch (this.instance?.type) {
case "authentik_stages_authenticator_duo.DuoDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsDuoUpdate({
id: this.instance?.pk,
id: parseInt(this.instance?.pk, 10),
duoDeviceRequest: device,
});
break;
case "authentik_stages_authenticator_sms.SMSDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsSmsUpdate({
id: this.instance?.pk,
id: parseInt(this.instance?.pk, 10),
sMSDeviceRequest: device,
});
break;
case "authentik_stages_authenticator_totp.TOTPDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsTotpUpdate({
id: this.instance?.pk,
id: parseInt(this.instance?.pk, 10),
tOTPDeviceRequest: device,
});
break;
case "authentik_stages_authenticator_static.StaticDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsStaticUpdate({
id: this.instance?.pk,
id: parseInt(this.instance?.pk, 10),
staticDeviceRequest: device,
});
break;
case "authentik_stages_authenticator_webauthn.WebAuthnDevice":
await new AuthenticatorsApi(DEFAULT_CONFIG).authenticatorsWebauthnUpdate({
id: this.instance?.pk,
id: parseInt(this.instance?.pk, 10),
webAuthnDeviceRequest: device,
});
break;
default:
break;
throw new SentryIgnoredError(
msg(str`Device type ${device.verboseName} cannot be edited`),
);
}
return device;
}

View File

@ -1,4 +1,5 @@
import { AndNext, DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { SentryIgnoredError } from "@goauthentik/common/errors";
import { deviceTypeName } from "@goauthentik/common/labels";
import { getRelativeTime } from "@goauthentik/common/utils";
import "@goauthentik/elements/buttons/Dropdown";
@ -10,7 +11,7 @@ import { PaginatedResponse, Table, TableColumn } from "@goauthentik/elements/tab
import "@goauthentik/user/user-settings/mfa/MFADeviceForm";
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
import { msg } from "@lit/localize";
import { msg, str } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
@ -89,7 +90,7 @@ export class MFADevicesPage extends Table<Device> {
async deleteWrapper(device: Device) {
const api = new AuthenticatorsApi(DEFAULT_CONFIG);
const id = { id: device.pk };
const id = { id: parseInt(device.pk, 10) };
switch (device.type) {
case "authentik_stages_authenticator_duo.DuoDevice":
return api.authenticatorsDuoDestroy(id);
@ -102,7 +103,9 @@ export class MFADevicesPage extends Table<Device> {
case "authentik_stages_authenticator_webauthn.WebAuthnDevice":
return api.authenticatorsWebauthnDestroy(id);
default:
break;
throw new SentryIgnoredError(
msg(str`Device type ${device.verboseName} cannot be deleted`),
);
}
}