Co-authored-by: Jens L <jens@goauthentik.io>
This commit is contained in:
![98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com](/assets/img/avatar_default.png)
committed by
GitHub

parent
cb80b76490
commit
0c05cd64bb
@ -6,3 +6,4 @@ dist
|
|||||||
coverage
|
coverage
|
||||||
src/locale-codes.ts
|
src/locale-codes.ts
|
||||||
storybook-static/
|
storybook-static/
|
||||||
|
src/locales/**
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { PFSize } from "@goauthentik/elements/Spinner";
|
import { PFSize } from "@goauthentik/elements/Spinner";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
||||||
@ -23,7 +23,17 @@ export class EmptyState extends AKElement {
|
|||||||
header = "";
|
header = "";
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [PFBase, PFEmptyState, PFTitle];
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFEmptyState,
|
||||||
|
PFTitle,
|
||||||
|
css`
|
||||||
|
i.pf-c-empty-state__icon {
|
||||||
|
height: var(--pf-global--icon--FontSize--2xl);
|
||||||
|
line-height: var(--pf-global--icon--FontSize--2xl);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
|
@ -15,7 +15,7 @@ import "@goauthentik/flow/sources/apple/AppleLoginInit";
|
|||||||
import "@goauthentik/flow/sources/plex/PlexLoginInit";
|
import "@goauthentik/flow/sources/plex/PlexLoginInit";
|
||||||
import "@goauthentik/flow/stages/FlowErrorStage";
|
import "@goauthentik/flow/stages/FlowErrorStage";
|
||||||
import "@goauthentik/flow/stages/RedirectStage";
|
import "@goauthentik/flow/stages/RedirectStage";
|
||||||
import { StageHost } from "@goauthentik/flow/stages/base";
|
import { StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
|
||||||
@ -189,12 +189,17 @@ export class FlowExecutor extends Interface implements StageHost {
|
|||||||
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
|
return globalAK()?.brand.uiTheme || UiThemeEnum.Automatic;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(payload?: FlowChallengeResponseRequest): Promise<boolean> {
|
async submit(
|
||||||
|
payload?: FlowChallengeResponseRequest,
|
||||||
|
options?: SubmitOptions,
|
||||||
|
): Promise<boolean> {
|
||||||
if (!payload) return Promise.reject();
|
if (!payload) return Promise.reject();
|
||||||
if (!this.challenge) return Promise.reject();
|
if (!this.challenge) return Promise.reject();
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
payload.component = this.challenge.component;
|
payload.component = this.challenge.component;
|
||||||
this.loading = true;
|
if (!options?.invisible) {
|
||||||
|
this.loading = true;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
|
const challenge = await new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
|
||||||
flowSlug: this.flowSlug,
|
flowSlug: this.flowSlug,
|
||||||
|
@ -40,6 +40,7 @@ export class AuthenticatorStaticStage extends BaseStage<
|
|||||||
columns: 2;
|
columns: 2;
|
||||||
-webkit-columns: 2;
|
-webkit-columns: 2;
|
||||||
-moz-columns: 2;
|
-moz-columns: 2;
|
||||||
|
column-width: 1em;
|
||||||
margin-left: var(--pf-global--spacer--xs);
|
margin-left: var(--pf-global--spacer--xs);
|
||||||
}
|
}
|
||||||
ul li {
|
ul li {
|
||||||
|
@ -2,13 +2,12 @@ import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|||||||
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode";
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageCode";
|
||||||
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo";
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageDuo";
|
||||||
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn";
|
import "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStageWebAuthn";
|
||||||
import { BaseStage, StageHost } from "@goauthentik/flow/stages/base";
|
import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/base";
|
||||||
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||||
@ -59,7 +58,7 @@ export class AuthenticatorValidateStage
|
|||||||
// We don't use this.submit here, as we don't want to advance the flow.
|
// We don't use this.submit here, as we don't want to advance the flow.
|
||||||
// We just want to notify the backend which challenge has been selected.
|
// We just want to notify the backend which challenge has been selected.
|
||||||
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
|
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
|
||||||
flowSlug: this.host.flowSlug || "",
|
flowSlug: this.host?.flowSlug || "",
|
||||||
query: window.location.search.substring(1),
|
query: window.location.search.substring(1),
|
||||||
flowChallengeResponseRequest: {
|
flowChallengeResponseRequest: {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -73,8 +72,11 @@ export class AuthenticatorValidateStage
|
|||||||
return this._selectedDeviceChallenge;
|
return this._selectedDeviceChallenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<boolean> {
|
submit(
|
||||||
return this.host?.submit(payload) || Promise.resolve();
|
payload: AuthenticatorValidationChallengeResponseRequest,
|
||||||
|
options?: SubmitOptions,
|
||||||
|
): Promise<boolean> {
|
||||||
|
return this.host?.submit(payload, options) || Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
@ -253,23 +255,7 @@ export class AuthenticatorValidateStage
|
|||||||
? this.renderDeviceChallenge()
|
? this.renderDeviceChallenge()
|
||||||
: html`<div class="pf-c-login__main-body">
|
: html`<div class="pf-c-login__main-body">
|
||||||
<form class="pf-c-form">
|
<form class="pf-c-form">
|
||||||
<ak-form-static
|
${this.renderUserInfo()}
|
||||||
class="pf-c-form__group"
|
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
|
||||||
user=${this.challenge.pendingUser}
|
|
||||||
>
|
|
||||||
<div slot="link">
|
|
||||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
|
||||||
>${msg("Not you?")}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</ak-form-static>
|
|
||||||
<input
|
|
||||||
name="username"
|
|
||||||
autocomplete="username"
|
|
||||||
type="hidden"
|
|
||||||
value="${this.challenge.pendingUser}"
|
|
||||||
/>
|
|
||||||
${this.selectedDeviceChallenge
|
${this.selectedDeviceChallenge
|
||||||
? ""
|
? ""
|
||||||
: html`<p>${msg("Select an authentication method.")}</p>`}
|
: html`<p>${msg("Select an authentication method.")}</p>`}
|
||||||
|
@ -1,59 +1,34 @@
|
|||||||
|
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
|
||||||
import "@goauthentik/elements/EmptyState";
|
import "@goauthentik/elements/EmptyState";
|
||||||
import "@goauthentik/elements/forms/FormElement";
|
import "@goauthentik/elements/forms/FormElement";
|
||||||
import "@goauthentik/flow/FormStatic";
|
|
||||||
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
|
|
||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
|
||||||
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|
||||||
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 {
|
import {
|
||||||
AuthenticatorValidationChallenge,
|
AuthenticatorValidationChallenge,
|
||||||
AuthenticatorValidationChallengeResponseRequest,
|
AuthenticatorValidationChallengeResponseRequest,
|
||||||
DeviceChallenge,
|
|
||||||
DeviceClassesEnum,
|
DeviceClassesEnum,
|
||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate-code")
|
@customElement("ak-stage-authenticator-validate-code")
|
||||||
export class AuthenticatorValidateStageWebCode extends BaseStage<
|
export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
|
||||||
AuthenticatorValidationChallenge,
|
AuthenticatorValidationChallenge,
|
||||||
AuthenticatorValidationChallengeResponseRequest
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
> {
|
> {
|
||||||
@property({ attribute: false })
|
|
||||||
deviceChallenge?: DeviceChallenge;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
showBackButton = false;
|
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
static get styles(): CSSResult[] {
|
||||||
return [
|
return super.styles.concat(css`
|
||||||
PFBase,
|
.icon-description {
|
||||||
PFLogin,
|
display: flex;
|
||||||
PFForm,
|
}
|
||||||
PFFormControl,
|
.icon-description i {
|
||||||
PFTitle,
|
font-size: 2em;
|
||||||
PFButton,
|
padding: 0.25em;
|
||||||
css`
|
padding-right: 0.5em;
|
||||||
.icon-description {
|
}
|
||||||
display: flex;
|
`);
|
||||||
}
|
|
||||||
.icon-description i {
|
|
||||||
font-size: 2em;
|
|
||||||
padding: 0.25em;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
@ -62,92 +37,62 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<
|
|||||||
</ak-empty-state>`;
|
</ak-empty-state>`;
|
||||||
}
|
}
|
||||||
return html`<div class="pf-c-login__main-body">
|
return html`<div class="pf-c-login__main-body">
|
||||||
<form
|
<form
|
||||||
class="pf-c-form"
|
class="pf-c-form"
|
||||||
@submit=${(e: Event) => {
|
@submit=${(e: Event) => {
|
||||||
this.submitForm(e);
|
this.submitForm(e);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
${this.renderUserInfo()}
|
||||||
|
<div class="icon-description">
|
||||||
|
<i
|
||||||
|
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
|
||||||
|
? "fa-key"
|
||||||
|
: "fa-mobile-alt"}"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
|
||||||
|
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
|
||||||
|
: html`<p>
|
||||||
|
${msg(
|
||||||
|
"Open your two-factor authenticator app to view your authentication code.",
|
||||||
|
)}
|
||||||
|
</p>`}
|
||||||
|
</div>
|
||||||
|
<ak-form-element
|
||||||
|
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||||
|
? msg("Static token")
|
||||||
|
: msg("Authentication code")}"
|
||||||
|
?required="${true}"
|
||||||
|
class="pf-c-form__group"
|
||||||
|
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
||||||
>
|
>
|
||||||
<ak-form-static
|
<!-- @ts-ignore -->
|
||||||
class="pf-c-form__group"
|
<input
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
type="text"
|
||||||
user=${this.challenge.pendingUser}
|
name="code"
|
||||||
>
|
inputmode="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||||
<div slot="link">
|
? "text"
|
||||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
: "numeric"}"
|
||||||
>${msg("Not you?")}</a
|
pattern="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
||||||
>
|
? "[0-9a-zA-Z]*"
|
||||||
</div>
|
: "[0-9]*"}"
|
||||||
</ak-form-static>
|
placeholder="${msg("Please enter your code")}"
|
||||||
<div class="icon-description">
|
autofocus=""
|
||||||
<i
|
autocomplete="one-time-code"
|
||||||
class="fa ${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
|
class="pf-c-form-control"
|
||||||
? "fa-key"
|
value="${PasswordManagerPrefill.totp || ""}"
|
||||||
: "fa-mobile-alt"}"
|
required
|
||||||
aria-hidden="true"
|
/>
|
||||||
></i>
|
</ak-form-element>
|
||||||
${this.deviceChallenge?.deviceClass == DeviceClassesEnum.Sms
|
|
||||||
? html`<p>${msg("A code has been sent to you via SMS.")}</p>`
|
|
||||||
: html`<p>
|
|
||||||
${msg(
|
|
||||||
"Open your two-factor authenticator app to view your authentication code.",
|
|
||||||
)}
|
|
||||||
</p>`}
|
|
||||||
</div>
|
|
||||||
<ak-form-element
|
|
||||||
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static
|
|
||||||
? msg("Static token")
|
|
||||||
: msg("Authentication code")}"
|
|
||||||
?required="${true}"
|
|
||||||
class="pf-c-form__group"
|
|
||||||
.errors=${(this.challenge?.responseErrors || {})["code"]}
|
|
||||||
>
|
|
||||||
<!-- @ts-ignore -->
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="code"
|
|
||||||
inputmode="${this.deviceChallenge?.deviceClass ===
|
|
||||||
DeviceClassesEnum.Static
|
|
||||||
? "text"
|
|
||||||
: "numeric"}"
|
|
||||||
pattern="${this.deviceChallenge?.deviceClass ===
|
|
||||||
DeviceClassesEnum.Static
|
|
||||||
? "[0-9a-zA-Z]*"
|
|
||||||
: "[0-9]*"}"
|
|
||||||
placeholder="${msg("Please enter your code")}"
|
|
||||||
autofocus=""
|
|
||||||
autocomplete="one-time-code"
|
|
||||||
class="pf-c-form-control"
|
|
||||||
value="${PasswordManagerPrefill.totp || ""}"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</ak-form-element>
|
|
||||||
|
|
||||||
<div class="pf-c-form__group pf-m-action">
|
<div class="pf-c-form__group pf-m-action">
|
||||||
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
|
||||||
${msg("Continue")}
|
${msg("Continue")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
${this.renderReturnToDevicePicker()}
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
<footer class="pf-c-login__main-footer">
|
</div>`;
|
||||||
<ul class="pf-c-login__main-footer-links">
|
|
||||||
${this.showBackButton
|
|
||||||
? html`<li class="pf-c-login__main-footer-links-item">
|
|
||||||
<button
|
|
||||||
class="pf-c-button pf-m-secondary pf-m-block"
|
|
||||||
@click=${() => {
|
|
||||||
if (!this.host) return;
|
|
||||||
(
|
|
||||||
this.host as AuthenticatorValidateStage
|
|
||||||
).selectedDeviceChallenge = undefined;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Return to device picker")}
|
|
||||||
</button>
|
|
||||||
</li>`
|
|
||||||
: html``}
|
|
||||||
</ul>
|
|
||||||
</footer>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,10 @@
|
|||||||
|
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
|
||||||
import "@goauthentik/elements/EmptyState";
|
import "@goauthentik/elements/EmptyState";
|
||||||
import "@goauthentik/elements/forms/FormElement";
|
import "@goauthentik/elements/forms/FormElement";
|
||||||
import "@goauthentik/flow/FormStatic";
|
|
||||||
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
|
|
||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
|
||||||
|
|
||||||
import { msg } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|
||||||
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 {
|
import {
|
||||||
AuthenticatorValidationChallenge,
|
AuthenticatorValidationChallenge,
|
||||||
@ -23,7 +13,7 @@ import {
|
|||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate-duo")
|
@customElement("ak-stage-authenticator-validate-duo")
|
||||||
export class AuthenticatorValidateStageWebDuo extends BaseStage<
|
export class AuthenticatorValidateStageWebDuo extends BaseDeviceStage<
|
||||||
AuthenticatorValidationChallenge,
|
AuthenticatorValidationChallenge,
|
||||||
AuthenticatorValidationChallengeResponseRequest
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
> {
|
> {
|
||||||
@ -33,14 +23,24 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
|
|||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
showBackButton = false;
|
showBackButton = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
@state()
|
||||||
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton];
|
authenticating = false;
|
||||||
}
|
|
||||||
|
|
||||||
firstUpdated(): void {
|
firstUpdated(): void {
|
||||||
this.host?.submit({
|
this.authenticating = true;
|
||||||
duo: this.deviceChallenge?.deviceUid,
|
this.host
|
||||||
});
|
?.submit(
|
||||||
|
{
|
||||||
|
duo: this.deviceChallenge?.deviceUid,
|
||||||
|
},
|
||||||
|
{ invisible: true },
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.authenticating = false;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.authenticating = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
@ -49,56 +49,25 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<
|
|||||||
</ak-empty-state>`;
|
</ak-empty-state>`;
|
||||||
}
|
}
|
||||||
const errors = this.challenge.responseErrors?.duo || [];
|
const errors = this.challenge.responseErrors?.duo || [];
|
||||||
|
const errorMessage = errors.map((err) => err.string);
|
||||||
return html`<div class="pf-c-login__main-body">
|
return html`<div class="pf-c-login__main-body">
|
||||||
<form
|
<form
|
||||||
class="pf-c-form"
|
class="pf-c-form"
|
||||||
@submit=${(e: Event) => {
|
@submit=${(e: Event) => {
|
||||||
this.submitForm(e);
|
this.submitForm(e);
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
${this.renderUserInfo()}
|
||||||
|
<ak-empty-state
|
||||||
|
?loading="${this.authenticating}"
|
||||||
|
header=${this.authenticating
|
||||||
|
? msg("Sending Duo push notification...")
|
||||||
|
: errorMessage.join(", ") || msg("Failed to authenticate")}
|
||||||
|
icon="fas fa-times"
|
||||||
>
|
>
|
||||||
<ak-form-static
|
</ak-empty-state>
|
||||||
class="pf-c-form__group"
|
<div class="pf-c-form__group pf-m-action">${this.renderReturnToDevicePicker()}</div>
|
||||||
userAvatar="${this.challenge.pendingUserAvatar}"
|
</form>
|
||||||
user=${this.challenge.pendingUser}
|
</div>`;
|
||||||
>
|
|
||||||
<div slot="link">
|
|
||||||
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
|
||||||
>${msg("Not you?")}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</ak-form-static>
|
|
||||||
|
|
||||||
${errors.length > 0
|
|
||||||
? errors.map((err) => {
|
|
||||||
if (err.code === "denied") {
|
|
||||||
return html` <ak-stage-access-denied-icon
|
|
||||||
errorMessage=${err.string}
|
|
||||||
>
|
|
||||||
</ak-stage-access-denied-icon>`;
|
|
||||||
}
|
|
||||||
return html`<p>${err.string}</p>`;
|
|
||||||
})
|
|
||||||
: html`${msg("Sending Duo push notification")}`}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<footer class="pf-c-login__main-footer">
|
|
||||||
<ul class="pf-c-login__main-footer-links">
|
|
||||||
${this.showBackButton
|
|
||||||
? html`<li class="pf-c-login__main-footer-links-item">
|
|
||||||
<button
|
|
||||||
class="pf-c-button pf-m-secondary pf-m-block"
|
|
||||||
@click=${() => {
|
|
||||||
if (!this.host) return;
|
|
||||||
(
|
|
||||||
this.host as AuthenticatorValidateStage
|
|
||||||
).selectedDeviceChallenge = undefined;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Return to device picker")}
|
|
||||||
</button>
|
|
||||||
</li>`
|
|
||||||
: html``}
|
|
||||||
</ul>
|
|
||||||
</footer>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
|
import { BaseDeviceStage } from "@goauthentik/app/flow/stages/authenticator_validate/base";
|
||||||
import {
|
import {
|
||||||
checkWebAuthnSupport,
|
checkWebAuthnSupport,
|
||||||
transformAssertionForServer,
|
transformAssertionForServer,
|
||||||
transformCredentialRequestOptions,
|
transformCredentialRequestOptions,
|
||||||
} from "@goauthentik/common/helpers/webauthn";
|
} from "@goauthentik/common/helpers/webauthn";
|
||||||
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
|
import "@goauthentik/elements/EmptyState";
|
||||||
import { BaseStage } from "@goauthentik/flow/stages/base";
|
|
||||||
|
|
||||||
import { msg, str } from "@lit/localize";
|
import { msg } from "@lit/localize";
|
||||||
import { CSSResult, TemplateResult, html } from "lit";
|
import { TemplateResult, html, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
|
||||||
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
|
|
||||||
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 PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
|
|
||||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthenticatorValidationChallenge,
|
AuthenticatorValidationChallenge,
|
||||||
@ -26,7 +17,7 @@ import {
|
|||||||
} from "@goauthentik/api";
|
} from "@goauthentik/api";
|
||||||
|
|
||||||
@customElement("ak-stage-authenticator-validate-webauthn")
|
@customElement("ak-stage-authenticator-validate-webauthn")
|
||||||
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
export class AuthenticatorValidateStageWebAuthn extends BaseDeviceStage<
|
||||||
AuthenticatorValidationChallenge,
|
AuthenticatorValidationChallenge,
|
||||||
AuthenticatorValidationChallengeResponseRequest
|
AuthenticatorValidationChallengeResponseRequest
|
||||||
> {
|
> {
|
||||||
@ -34,25 +25,15 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
|||||||
deviceChallenge?: DeviceChallenge;
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
@property()
|
@property()
|
||||||
authenticateMessage?: string;
|
errorMessage?: string;
|
||||||
|
|
||||||
@property({ type: Boolean })
|
@property({ type: Boolean })
|
||||||
showBackButton = false;
|
showBackButton = false;
|
||||||
|
|
||||||
transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions;
|
@state()
|
||||||
|
authenticating = false;
|
||||||
|
|
||||||
static get styles(): CSSResult[] {
|
transformedCredentialRequestOptions?: PublicKeyCredentialRequestOptions;
|
||||||
return [
|
|
||||||
PFBase,
|
|
||||||
PFLogin,
|
|
||||||
PFEmptyState,
|
|
||||||
PFBullseye,
|
|
||||||
PFForm,
|
|
||||||
PFFormControl,
|
|
||||||
PFTitle,
|
|
||||||
PFButton,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async authenticate(): Promise<void> {
|
async authenticate(): Promise<void> {
|
||||||
// request the authenticator to create an assertion signature using the
|
// request the authenticator to create an assertion signature using the
|
||||||
@ -64,10 +45,10 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
|||||||
publicKey: this.transformedCredentialRequestOptions,
|
publicKey: this.transformedCredentialRequestOptions,
|
||||||
});
|
});
|
||||||
if (!assertion) {
|
if (!assertion) {
|
||||||
throw new Error(msg("Assertions is empty"));
|
throw new Error("Assertions is empty");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(msg(str`Error when creating credential: ${err}`));
|
throw new Error(`Error when creating credential: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we now have an authentication assertion! encode the byte arrays contained
|
// we now have an authentication assertion! encode the byte arrays contained
|
||||||
@ -78,11 +59,16 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
|||||||
|
|
||||||
// post the assertion to the server for verification.
|
// post the assertion to the server for verification.
|
||||||
try {
|
try {
|
||||||
await this.host?.submit({
|
await this.host?.submit(
|
||||||
webauthn: transformedAssertionForServer,
|
{
|
||||||
});
|
webauthn: transformedAssertionForServer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
invisible: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(msg(str`Error when validating assertion on server: ${err}`));
|
throw new Error(`Error when validating assertion on server: ${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,58 +83,46 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async authenticateWrapper(): Promise<void> {
|
async authenticateWrapper(): Promise<void> {
|
||||||
if (this.host.loading) {
|
if (this.authenticating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.host.loading = true;
|
this.authenticating = true;
|
||||||
this.authenticate()
|
this.authenticate()
|
||||||
.catch((e) => {
|
.catch((e: Error) => {
|
||||||
console.error(e);
|
console.warn(`authentik/flows/authenticator_validate/webauthn: ${e.toString()}`);
|
||||||
this.authenticateMessage = e.toString();
|
this.errorMessage = msg("Authentication failed.");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.host.loading = false;
|
this.authenticating = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-login__main-body">
|
return html`<div class="pf-c-login__main-body">
|
||||||
${this.authenticateMessage
|
<form class="pf-c-form">
|
||||||
? html`<div class="pf-c-form__group pf-m-action">
|
${this.renderUserInfo()}
|
||||||
<p class="pf-m-block">${this.authenticateMessage}</p>
|
<ak-empty-state
|
||||||
<button
|
?loading="${this.authenticating}"
|
||||||
|
header=${this.authenticating
|
||||||
|
? msg("Authenticating...")
|
||||||
|
: this.errorMessage || msg("Failed to authenticate")}
|
||||||
|
icon="fa-times"
|
||||||
|
>
|
||||||
|
</ak-empty-state>
|
||||||
|
<div class="pf-c-form__group pf-m-action">
|
||||||
|
${this.errorMessage
|
||||||
|
? html` <button
|
||||||
class="pf-c-button pf-m-primary pf-m-block"
|
class="pf-c-button pf-m-primary pf-m-block"
|
||||||
@click=${() => {
|
@click=${() => {
|
||||||
this.authenticateWrapper();
|
this.authenticateWrapper();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
${msg("Retry authentication")}
|
${msg("Retry authentication")}
|
||||||
</button>
|
</button>`
|
||||||
</div>`
|
: nothing}
|
||||||
: html`<div class="pf-c-form__group pf-m-action">
|
${this.renderReturnToDevicePicker()}
|
||||||
<p class="pf-m-block"> </p>
|
</div>
|
||||||
<p class="pf-m-block"> </p>
|
</form>
|
||||||
<p class="pf-m-block"> </p>
|
</div>`;
|
||||||
</div> `}
|
|
||||||
</div>
|
|
||||||
<footer class="pf-c-login__main-footer">
|
|
||||||
<ul class="pf-c-login__main-footer-links">
|
|
||||||
${this.showBackButton
|
|
||||||
? html`<li class="pf-c-login__main-footer-links-item">
|
|
||||||
<button
|
|
||||||
class="pf-c-button pf-m-secondary pf-m-block"
|
|
||||||
@click=${() => {
|
|
||||||
if (!this.host) return;
|
|
||||||
(
|
|
||||||
this.host as AuthenticatorValidateStage
|
|
||||||
).selectedDeviceChallenge = undefined;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${msg("Return to device picker")}
|
|
||||||
</button>
|
|
||||||
</li>`
|
|
||||||
: html``}
|
|
||||||
</ul>
|
|
||||||
</footer>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
69
web/src/flow/stages/authenticator_validate/base.ts
Normal file
69
web/src/flow/stages/authenticator_validate/base.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import {
|
||||||
|
BaseStage,
|
||||||
|
FlowInfoChallenge,
|
||||||
|
PendingUserChallenge,
|
||||||
|
} from "@goauthentik/app/flow/stages/base";
|
||||||
|
import { AuthenticatorValidateStage } from "@goauthentik/flow/stages/authenticator_validate/AuthenticatorValidateStage";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
|
import { property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||||
|
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 { DeviceChallenge } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export class BaseDeviceStage<
|
||||||
|
Tin extends FlowInfoChallenge & PendingUserChallenge,
|
||||||
|
Tout,
|
||||||
|
> extends BaseStage<Tin, Tout> {
|
||||||
|
@property({ attribute: false })
|
||||||
|
deviceChallenge?: DeviceChallenge;
|
||||||
|
|
||||||
|
@property({ type: Boolean })
|
||||||
|
showBackButton = false;
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
PFBase,
|
||||||
|
PFLogin,
|
||||||
|
PFForm,
|
||||||
|
PFFormControl,
|
||||||
|
PFTitle,
|
||||||
|
PFButton,
|
||||||
|
css`
|
||||||
|
.pf-c-form__group.pf-m-action {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: calc(var(--pf-c-form__group--m-action--MarginTop) / 2);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(payload: Tin): Promise<boolean> {
|
||||||
|
return this.host?.submit(payload) || Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderReturnToDevicePicker(): TemplateResult {
|
||||||
|
if (!this.showBackButton) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`<button
|
||||||
|
class="pf-c-button pf-m-secondary pf-m-block"
|
||||||
|
@click=${() => {
|
||||||
|
if (!this.host) return;
|
||||||
|
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${msg("Return to device picker")}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,22 @@
|
|||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
import { KeyUnknown } from "@goauthentik/elements/forms/Form";
|
||||||
|
|
||||||
|
import { msg } from "@lit/localize";
|
||||||
import { TemplateResult, html } from "lit";
|
import { TemplateResult, html } from "lit";
|
||||||
import { property } from "lit/decorators.js";
|
import { property } from "lit/decorators.js";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
|
|
||||||
import { CurrentBrand, ErrorDetail } from "@goauthentik/api";
|
import { ContextualFlowInfo, CurrentBrand, ErrorDetail } from "@goauthentik/api";
|
||||||
|
|
||||||
|
export interface SubmitOptions {
|
||||||
|
invisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface StageHost {
|
export interface StageHost {
|
||||||
challenge?: unknown;
|
challenge?: unknown;
|
||||||
flowSlug?: string;
|
flowSlug?: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
submit(payload: unknown): Promise<boolean>;
|
submit(payload: unknown, options?: SubmitOptions): Promise<boolean>;
|
||||||
|
|
||||||
readonly brand?: CurrentBrand;
|
readonly brand?: CurrentBrand;
|
||||||
}
|
}
|
||||||
@ -26,7 +32,21 @@ export function readFileAsync(file: Blob) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BaseStage<Tin, Tout> extends AKElement {
|
// Challenge which contains flow info
|
||||||
|
export interface FlowInfoChallenge {
|
||||||
|
flowInfo?: ContextualFlowInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Challenge which has a pending user
|
||||||
|
export interface PendingUserChallenge {
|
||||||
|
pendingUser?: string;
|
||||||
|
pendingUserAvatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseStage<
|
||||||
|
Tin extends FlowInfoChallenge & PendingUserChallenge,
|
||||||
|
Tout,
|
||||||
|
> extends AKElement {
|
||||||
host!: StageHost;
|
host!: StageHost;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@ -68,6 +88,31 @@ export class BaseStage<Tin, Tout> extends AKElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderUserInfo(): TemplateResult {
|
||||||
|
if (!this.challenge.pendingUser || !this.challenge.pendingUserAvatar) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<ak-form-static
|
||||||
|
class="pf-c-form__group"
|
||||||
|
userAvatar="${this.challenge.pendingUserAvatar}"
|
||||||
|
user=${this.challenge.pendingUser}
|
||||||
|
>
|
||||||
|
<div slot="link">
|
||||||
|
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
|
||||||
|
>${msg("Not you?")}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</ak-form-static>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
autocomplete="username"
|
||||||
|
type="hidden"
|
||||||
|
value="${this.challenge.pendingUser}"
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
cleanup(): void {
|
cleanup(): void {
|
||||||
// Method that can be overridden by stages
|
// Method that can be overridden by stages
|
||||||
return;
|
return;
|
||||||
|
Reference in New Issue
Block a user