web: re-format with prettier

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer
2021-08-03 17:52:21 +02:00
parent 77ed25ae34
commit 2c60ec50be
218 changed files with 11696 additions and 8225 deletions

View File

@ -1,5 +1,13 @@
import { t } from "@lingui/macro";
import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element";
import {
LitElement,
html,
customElement,
property,
TemplateResult,
CSSResult,
css,
} from "lit-element";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
@ -26,7 +34,15 @@ import "./stages/password/PasswordStage";
import "./stages/prompt/PromptStage";
import "./sources/plex/PlexLoginInit";
import { StageHost } from "./stages/base";
import { ChallengeChoices, CurrentTenant, ChallengeTypes, FlowChallengeResponseRequest, FlowsApi, RedirectChallenge, ShellChallenge } from "authentik-api";
import {
ChallengeChoices,
CurrentTenant,
ChallengeTypes,
FlowChallengeResponseRequest,
FlowsApi,
RedirectChallenge,
ShellChallenge,
} from "authentik-api";
import { DEFAULT_CONFIG, tenant } from "../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
import { until } from "lit-html/directives/until";
@ -37,13 +53,12 @@ import { WebsocketClient } from "../common/ws";
@customElement("ak-flow-executor")
export class FlowExecutor extends LitElement implements StageHost {
flowSlug: string;
@property({attribute: false})
@property({ attribute: false })
challenge?: ChallengeTypes;
@property({type: Boolean})
@property({ type: Boolean })
loading = false;
@property({ attribute: false })
@ -84,13 +99,15 @@ export class FlowExecutor extends LitElement implements StageHost {
}
setBackground(url: string): void {
this.shadowRoot?.querySelectorAll<HTMLDivElement>(".pf-c-background-image").forEach((bg) => {
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
});
this.shadowRoot
?.querySelectorAll<HTMLDivElement>(".pf-c-background-image")
.forEach((bg) => {
bg.style.setProperty("--ak-flow-background", `url('${url}')`);
});
}
private postUpdate(): void {
tenant().then(tenant => {
tenant().then((tenant) => {
if (this.challenge?.flowInfo?.title) {
document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`;
} else {
@ -105,40 +122,48 @@ export class FlowExecutor extends LitElement implements StageHost {
// @ts-ignore
payload.component = this.challenge.component;
this.loading = true;
return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
flowChallengeResponseRequest: payload,
}).then((data) => {
this.challenge = data;
this.postUpdate();
}).catch((e: Error | Response) => {
this.errorMessage(e);
}).finally(() => {
this.loading = false;
});
return new FlowsApi(DEFAULT_CONFIG)
.flowsExecutorSolve({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
flowChallengeResponseRequest: payload,
})
.then((data) => {
this.challenge = data;
this.postUpdate();
})
.catch((e: Error | Response) => {
this.errorMessage(e);
})
.finally(() => {
this.loading = false;
});
}
firstUpdated(): void {
configureSentry();
tenant().then(tenant => this.tenant = tenant);
tenant().then((tenant) => (this.tenant = tenant));
this.loading = true;
new FlowsApi(DEFAULT_CONFIG).flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
}).then((challenge) => {
this.challenge = challenge;
// Only set background on first update, flow won't change throughout execution
if (this.challenge?.flowInfo?.background) {
this.setBackground(this.challenge.flowInfo.background);
}
this.postUpdate();
}).catch((e: Error | Response) => {
// Catch JSON or Update errors
this.errorMessage(e);
}).finally(() => {
this.loading = false;
});
new FlowsApi(DEFAULT_CONFIG)
.flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
})
.then((challenge) => {
this.challenge = challenge;
// Only set background on first update, flow won't change throughout execution
if (this.challenge?.flowInfo?.background) {
this.setBackground(this.challenge.flowInfo.background);
}
this.postUpdate();
})
.catch((e: Error | Response) => {
// Catch JSON or Update errors
this.errorMessage(e);
})
.finally(() => {
this.loading = false;
});
}
async errorMessage(error: Error | Response): Promise<void> {
@ -167,7 +192,7 @@ export class FlowExecutor extends LitElement implements StageHost {
</a>
</li>
</ul>
</footer>`
</footer>`,
} as ChallengeTypes;
}
@ -183,46 +208,92 @@ export class FlowExecutor extends LitElement implements StageHost {
}
switch (this.challenge.type) {
case ChallengeChoices.Redirect:
console.debug("authentik/flows: redirecting to url from server", (this.challenge as RedirectChallenge).to);
console.debug(
"authentik/flows: redirecting to url from server",
(this.challenge as RedirectChallenge).to,
);
window.location.assign((this.challenge as RedirectChallenge).to);
return html`<ak-empty-state
?loading=${true}
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}>
</ak-empty-state>`;
case ChallengeChoices.Shell:
return html`${unsafeHTML((this.challenge as ShellChallenge).body)}`;
case ChallengeChoices.Native:
switch (this.challenge.component) {
case "ak-stage-access-denied":
return html`<ak-stage-access-denied .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-access-denied>`;
return html`<ak-stage-access-denied
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-access-denied>`;
case "ak-stage-identification":
return html`<ak-stage-identification .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-identification>`;
return html`<ak-stage-identification
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-identification>`;
case "ak-stage-password":
return html`<ak-stage-password .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-password>`;
return html`<ak-stage-password
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-password>`;
case "ak-stage-captcha":
return html`<ak-stage-captcha .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-captcha>`;
return html`<ak-stage-captcha
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-captcha>`;
case "ak-stage-consent":
return html`<ak-stage-consent .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-consent>`;
return html`<ak-stage-consent
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-consent>`;
case "ak-stage-dummy":
return html`<ak-stage-dummy .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-dummy>`;
return html`<ak-stage-dummy
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-dummy>`;
case "ak-stage-email":
return html`<ak-stage-email .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-email>`;
return html`<ak-stage-email
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-email>`;
case "ak-stage-autosubmit":
return html`<ak-stage-autosubmit .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-autosubmit>`;
return html`<ak-stage-autosubmit
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-autosubmit>`;
case "ak-stage-prompt":
return html`<ak-stage-prompt .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-prompt>`;
return html`<ak-stage-prompt
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-prompt>`;
case "ak-stage-authenticator-totp":
return html`<ak-stage-authenticator-totp .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-totp>`;
return html`<ak-stage-authenticator-totp
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-totp>`;
case "ak-stage-authenticator-duo":
return html`<ak-stage-authenticator-duo .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-duo>`;
return html`<ak-stage-authenticator-duo
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-duo>`;
case "ak-stage-authenticator-static":
return html`<ak-stage-authenticator-static .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-static>`;
return html`<ak-stage-authenticator-static
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-static>`;
case "ak-stage-authenticator-webauthn":
return html`<ak-stage-authenticator-webauthn .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-webauthn>`;
return html`<ak-stage-authenticator-webauthn
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-webauthn>`;
case "ak-stage-authenticator-validate":
return html`<ak-stage-authenticator-validate .host=${this as StageHost} .challenge=${this.challenge}></ak-stage-authenticator-validate>`;
return html`<ak-stage-authenticator-validate
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-stage-authenticator-validate>`;
case "ak-flow-sources-plex":
return html`<ak-flow-sources-plex .host=${this as StageHost} .challenge=${this.challenge}></ak-flow-sources-plex>`;
return html`<ak-flow-sources-plex
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-plex>`;
default:
break;
}
@ -236,59 +307,85 @@ export class FlowExecutor extends LitElement implements StageHost {
renderChallengeWrapper(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading=${true}
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading=${true} header=${t`Loading`}> </ak-empty-state>`;
}
return html`
${this.loading ? this.renderLoading() : html``}
${this.renderChallenge()}
`;
return html` ${this.loading ? this.renderLoading() : html``} ${this.renderChallenge()} `;
}
render(): TemplateResult {
return html`<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feColorMatrix in="SourceGraphic" type="matrix" values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0" />
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
<div class="pf-c-login">
<div class="ak-login-container">
<header class="pf-c-login__header">
<div class="pf-c-brand ak-brand">
<img src="${ifDefined(this.tenant?.brandingLogo)}" alt="authentik icon" />
</div>
</header>
<div class="pf-c-login__main">
${this.renderChallengeWrapper()}
</div>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
${until(this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}">${link.name}</a>
</li>`;
}))}
${this.tenant?.brandingTitle != "authentik" ? html`
<li><a href="https://goauthentik.io">${t`Powered by authentik`}</a></li>
` : html``}
${this.challenge?.flowInfo?.background?.startsWith("/static") ? html`
<li><a href="https://unsplash.com/@ventiviews">${t`Background image`}</a></li>
` : html``}
</ul>
</footer>
<svg
xmlns="http://www.w3.org/2000/svg"
class="pf-c-background-image__filter"
width="0"
height="0"
>
<filter id="image_overlay">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="1.3 0 0 0 0 0 1.3 0 0 0 0 0 1.3 0 0 0 0 0 1 0"
/>
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR
type="table"
tableValues="0.086274509803922 0.43921568627451"
></feFuncR>
<feFuncG
type="table"
tableValues="0.086274509803922 0.43921568627451"
></feFuncG>
<feFuncB
type="table"
tableValues="0.086274509803922 0.43921568627451"
></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
</div>`;
<div class="pf-c-login">
<div class="ak-login-container">
<header class="pf-c-login__header">
<div class="pf-c-brand ak-brand">
<img
src="${ifDefined(this.tenant?.brandingLogo)}"
alt="authentik icon"
/>
</div>
</header>
<div class="pf-c-login__main">${this.renderChallengeWrapper()}</div>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
${until(
this.tenant?.uiFooterLinks?.map((link) => {
return html`<li>
<a href="${link.href || ""}">${link.name}</a>
</li>`;
}),
)}
${this.tenant?.brandingTitle != "authentik"
? html`
<li>
<a href="https://goauthentik.io"
>${t`Powered by authentik`}</a
>
</li>
`
: html``}
${this.challenge?.flowInfo?.background?.startsWith("/static")
? html`
<li>
<a href="https://unsplash.com/@ventiviews"
>${t`Background image`}</a
>
</li>
`
: html``}
</ul>
</footer>
</div>
</div>`;
}
}

View File

@ -1,11 +1,18 @@
import { t } from "@lingui/macro";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import PFAvatar from "@patternfly/patternfly/components/Avatar/avatar.css";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-form-static")
export class FormStatic extends LitElement {
@property()
userAvatar?: string;
@ -13,39 +20,45 @@ export class FormStatic extends LitElement {
user = "";
static get styles(): CSSResult[] {
return [PFAvatar, css`
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-static .avatar {
display: flex;
align-items: center;
}
.form-control-static img {
margin-right: var(--pf-global--spacer--xs);
}
.form-control-static a {
padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl);
}
`];
return [
PFAvatar,
css`
/* Form with user */
.form-control-static {
margin-top: var(--pf-global--spacer--sm);
display: flex;
align-items: center;
justify-content: space-between;
}
.form-control-static .avatar {
display: flex;
align-items: center;
}
.form-control-static img {
margin-right: var(--pf-global--spacer--xs);
}
.form-control-static a {
padding-top: var(--pf-global--spacer--xs);
padding-bottom: var(--pf-global--spacer--xs);
line-height: var(--pf-global--spacer--xl);
}
`,
];
}
render(): TemplateResult {
return html`
<div class="form-control-static">
<div class="avatar">
<img class="pf-c-avatar" src="${ifDefined(this.userAvatar)}" alt="${t`User's avatar`}">
<img
class="pf-c-avatar"
src="${ifDefined(this.userAvatar)}"
alt="${t`User's avatar`}"
/>
${this.user}
</div>
<slot name="link"></slot>
</div>
`;
}
}

View File

@ -13,23 +13,20 @@ import { t } from "@lingui/macro";
import "../../elements/EmptyState";
@customElement("ak-stage-access-denied")
export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChallengeResponseRequest> {
export class FlowAccessDenied extends BaseStage<
AccessDeniedChallenge,
FlowChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFList, PFFormControl, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form method="POST" class="pf-c-form">
@ -39,15 +36,13 @@ export class FlowAccessDenied extends BaseStage<AccessDeniedChallenge, FlowChall
${t`Request has been denied.`}
</p>
${this.challenge?.errorMessage &&
html`<hr>
html`<hr />
<p>${this.challenge.errorMessage}</p>`}
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -23,40 +23,54 @@ export const DEFAULT_HEADERS = {
};
export function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
const top = (screen.height - h) / 4, left = (screen.width - w) / 2;
const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
const top = (screen.height - h) / 4,
left = (screen.width - w) / 2;
const popup = window.open(
url,
title,
`scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`,
);
return popup;
}
export class PlexAPIClient {
token: string;
constructor(token: string) {
this.token = token;
}
static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
const headers = { ...DEFAULT_HEADERS, ...{
"X-Plex-Client-Identifier": clientIdentifier
}};
static async getPin(
clientIdentifier: string,
): Promise<{ authUrl: string; pin: PlexPinResponse }> {
const headers = {
...DEFAULT_HEADERS,
...{
"X-Plex-Client-Identifier": clientIdentifier,
},
};
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
method: "POST",
headers: headers
headers: headers,
});
const pin: PlexPinResponse = await pinResponse.json();
return {
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
pin: pin
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(
clientIdentifier,
)}&code=${pin.code}`,
pin: pin,
};
}
static async pinStatus(clientIdentifier: string, id: number): Promise<string | undefined> {
const headers = { ...DEFAULT_HEADERS, ...{
"X-Plex-Client-Identifier": clientIdentifier
}};
const headers = {
...DEFAULT_HEADERS,
...{
"X-Plex-Client-Identifier": clientIdentifier,
},
};
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
headers: headers
headers: headers,
});
const pin: PlexPinResponse = await pinResponse.json();
return pin.authToken || "";
@ -65,7 +79,7 @@ export class PlexAPIClient {
static async pinPoll(clientIdentifier: string, id: number): Promise<string> {
const executePoll = async (
resolve: (authToken: string) => void,
reject: (e: Error) => void
reject: (e: Error) => void,
) => {
try {
const response = await PlexAPIClient.pinStatus(clientIdentifier, id);
@ -84,13 +98,15 @@ export class PlexAPIClient {
}
async getServers(): Promise<PlexResource[]> {
const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
headers: DEFAULT_HEADERS
});
const resourcesResponse = await fetch(
`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`,
{
headers: DEFAULT_HEADERS,
},
);
const resources: PlexResource[] = await resourcesResponse.json();
return resources.filter(r => {
return resources.filter((r) => {
return r.provides.toLowerCase().includes("server") && r.owned;
});
}
}

View File

@ -1,5 +1,8 @@
import { t } from "@lingui/macro";
import { PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest } from "authentik-api";
import {
PlexAuthenticationChallenge,
PlexAuthenticationChallengeResponseRequest,
} from "authentik-api";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
@ -16,10 +19,11 @@ import { SourcesApi } from "authentik-api";
import { showMessage } from "../../../elements/messages/MessageContainer";
import { MessageLevel } from "../../../elements/messages/Message";
@customElement("ak-flow-sources-plex")
export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAuthenticationChallengeResponseRequest> {
export class PlexLoginInit extends BaseStage<
PlexAuthenticationChallenge,
PlexAuthenticationChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
@ -27,46 +31,43 @@ export class PlexLoginInit extends BaseStage<PlexAuthenticationChallenge, PlexAu
async firstUpdated(): Promise<void> {
const authInfo = await PlexAPIClient.getPin(this.challenge?.clientId || "");
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then(token => {
PlexAPIClient.pinPoll(this.challenge?.clientId || "", authInfo.pin.id).then((token) => {
authWindow?.close();
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRedeemTokenCreate({
plexTokenRedeemRequest: {
plexToken: token,
},
slug: this.challenge?.slug || "",
}).then((r) => {
window.location.assign(r.to);
}).catch((r: Response) => {
r.json().then((body: {detail: string}) => {
showMessage({
level: MessageLevel.error,
message: body.detail
new SourcesApi(DEFAULT_CONFIG)
.sourcesPlexRedeemTokenCreate({
plexTokenRedeemRequest: {
plexToken: token,
},
slug: this.challenge?.slug || "",
})
.then((r) => {
window.location.assign(r.to);
})
.catch((r: Response) => {
r.json().then((body: { detail: string }) => {
showMessage({
level: MessageLevel.error,
message: body.detail,
});
setTimeout(() => {
window.location.assign("/");
}, 5000);
});
setTimeout(() => {
window.location.assign("/");
}, 5000);
});
});
});
}
render(): TemplateResult {
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${t`Authenticating with Plex...`}
</h1>
<h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Plex...`}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-empty-state
?loading="${true}">
</ak-empty-state>
<ak-empty-state ?loading="${true}"> </ak-empty-state>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -11,13 +11,19 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest, StagesApi } from "authentik-api";
import {
AuthenticatorDuoChallenge,
AuthenticatorDuoChallengeResponseRequest,
StagesApi,
} from "authentik-api";
import { DEFAULT_CONFIG } from "../../../api/Config";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-duo")
export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge, AuthenticatorDuoChallengeResponseRequest> {
export class AuthenticatorDuoStage extends BaseStage<
AuthenticatorDuoChallenge,
AuthenticatorDuoChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
@ -31,35 +37,41 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
}
checkEnrollStatus(): Promise<void> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stageUuid || "",
}).then(() => {
this.host?.submit({});
}).catch(() => {
console.debug("authentik/flows/duo: Waiting for auth status");
});
return new StagesApi(DEFAULT_CONFIG)
.stagesAuthenticatorDuoEnrollmentStatusCreate({
stageUuid: this.challenge?.stageUuid || "",
})
.then(() => {
this.host?.submit({});
})
.catch(() => {
console.debug("authentik/flows/duo: Waiting for auth status");
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<img src=${this.challenge.activationBarcode} />
@ -69,18 +81,20 @@ export class AuthenticatorDuoStage extends BaseStage<AuthenticatorDuoChallenge,
<a href=${this.challenge.activationCode}>${t`Duo activation`}</a>
<div class="pf-c-form__group pf-m-action">
<button type="button" class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.checkEnrollStatus();
}}>
<button
type="button"
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.checkEnrollStatus();
}}
>
${t`Check status`}
</button>
</div>
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -11,58 +11,75 @@ import { BaseStage } from "../base";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest } from "authentik-api";
import {
AuthenticatorStaticChallenge,
AuthenticatorStaticChallengeResponseRequest,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
export const STATIC_TOKEN_STYLE = 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;
}
/* 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;
}
`;
@customElement("ak-stage-authenticator-static")
export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChallenge, AuthenticatorStaticChallengeResponseRequest> {
export class AuthenticatorStaticStage extends BaseStage<
AuthenticatorStaticChallenge,
AuthenticatorStaticChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal, STATIC_TOKEN_STYLE];
return [
PFBase,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
AKGlobal,
STATIC_TOKEN_STYLE,
];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<ak-form-element
label="${t`Tokens`}"
?required="${true}"
class="pf-c-form__group">
class="pf-c-form__group"
>
<ul class="ak-otp-tokens">
${this.challenge.codes.map((token) => {
return html`<li>${token}</li>`;
@ -78,9 +95,7 @@ export class AuthenticatorStaticStage extends BaseStage<AuthenticatorStaticChall
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -14,53 +14,66 @@ import { showMessage } from "../../../elements/messages/MessageContainer";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { MessageLevel } from "../../../elements/messages/Message";
import { AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest } from "authentik-api";
import {
AuthenticatorTOTPChallenge,
AuthenticatorTOTPChallengeResponseRequest,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-totp")
export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge, AuthenticatorTOTPChallengeResponseRequest> {
export class AuthenticatorTOTPStage extends BaseStage<
AuthenticatorTOTPChallenge,
AuthenticatorTOTPChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<input type="hidden" name="otp_uri" value=${this.challenge.configUrl} />
<ak-form-element>
<!-- @ts-ignore -->
<qr-code data="${this.challenge.configUrl}"></qr-code>
<button type="button" class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress" @click=${(e: Event) => {
e.preventDefault();
if (!this.challenge?.configUrl) return;
navigator.clipboard.writeText(this.challenge?.configUrl).then(() => {
showMessage({
level: MessageLevel.success,
message: t`Successfully copied TOTP Config.`
});
});
}}>
<button
type="button"
class="pf-c-button pf-m-secondary pf-m-progress pf-m-in-progress"
@click=${(e: Event) => {
e.preventDefault();
if (!this.challenge?.configUrl) return;
navigator.clipboard
.writeText(this.challenge?.configUrl)
.then(() => {
showMessage({
level: MessageLevel.success,
message: t`Successfully copied TOTP Config.`,
});
});
}}
>
<span class="pf-c-button__progress"><i class="fas fa-copy"></i></span>
${t`Copy`}
</button>
@ -69,9 +82,11 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}>
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input type="text"
<input
type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
@ -79,7 +94,8 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
required>
required
/>
</ak-form-element>
<div class="pf-c-form__group pf-m-action">
@ -90,9 +106,7 @@ export class AuthenticatorTOTPStage extends BaseStage<AuthenticatorTOTPChallenge
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -12,7 +12,11 @@ import "./AuthenticatorValidateStageWebAuthn";
import "./AuthenticatorValidateStageCode";
import "./AuthenticatorValidateStageDuo";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
export enum DeviceClasses {
STATIC = "static",
@ -22,9 +26,14 @@ export enum DeviceClasses {
}
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> implements StageHost {
@property({attribute: false})
export class AuthenticatorValidateStage
extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
>
implements StageHost
{
@property({ attribute: false })
selectedDeviceChallenge?: DeviceChallenge;
submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise<void> {
@ -65,7 +74,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
return html`<i class="fas fa-mobile-alt"></i>
<div class="right">
<p>${t`Duo push-notifications`}</p>
<small>${t`Receive a push notification on your phone to prove your identity.`}</small>
<small
>${t`Receive a push notification on your phone to prove your identity.`}</small
>
</div>`;
case DeviceClasses.WEBAUTHN:
return html`<i class="fas fa-mobile-alt"></i>
@ -78,7 +89,9 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
// and we have a pre-filled value from the password manager,
// directly set the the TOTP device Challenge as active.
if (PasswordManagerPrefill.totp) {
console.debug("authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge");
console.debug(
"authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
);
this.selectedDeviceChallenge = deviceChallenge;
// Delay the update as a re-render isn't triggered from here
setTimeout(() => {
@ -103,13 +116,16 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
}
renderDevicePicker(): TemplateResult {
return html`
<ul>
return html` <ul>
${this.challenge?.deviceChallenges.map((challenges) => {
return html`<li>
<button class="pf-c-button authenticator-button" type="button" @click=${() => {
this.selectedDeviceChallenge = challenges;
}}>
<button
class="pf-c-button authenticator-button"
type="button"
@click=${() => {
this.selectedDeviceChallenge = challenges;
}}
>
${this.renderDevicePickerSingle(challenges)}
</button>
</li>`;
@ -122,60 +138,56 @@ export class AuthenticatorValidateStage extends BaseStage<AuthenticatorValidatio
return html``;
}
switch (this.selectedDeviceChallenge?.deviceClass) {
case DeviceClasses.STATIC:
case DeviceClasses.TOTP:
return html`<ak-stage-authenticator-validate-code
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-webauthn>`;
case DeviceClasses.DUO:
return html`<ak-stage-authenticator-validate-duo
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}>
</ak-stage-authenticator-validate-duo>`;
case DeviceClasses.STATIC:
case DeviceClasses.TOTP:
return html`<ak-stage-authenticator-validate-code
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
>
</ak-stage-authenticator-validate-code>`;
case DeviceClasses.WEBAUTHN:
return html`<ak-stage-authenticator-validate-webauthn
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
>
</ak-stage-authenticator-validate-webauthn>`;
case DeviceClasses.DUO:
return html`<ak-stage-authenticator-validate-duo
.host=${this}
.challenge=${this.challenge}
.deviceChallenge=${this.selectedDeviceChallenge}
.showBackButton=${(this.challenge?.deviceChallenges.length || []) > 1}
>
</ak-stage-authenticator-validate-duo>`;
}
return html``;
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
// User only has a single device class, so we don't show a picker
if (this.challenge?.deviceChallenges.length === 1) {
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
${this.challenge.flowInfo?.title}
</h1>
${this.selectedDeviceChallenge ? "" : html`<p class="pf-c-login__main-header-desc">
${t`Select an identification method.`}
</p>`}
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
${this.selectedDeviceChallenge
? ""
: html`<p class="pf-c-login__main-header-desc">
${t`Select an identification method.`}
</p>`}
</header>
${this.selectedDeviceChallenge ?
this.renderDeviceChallenge() :
html`<div class="pf-c-login__main-body">
${this.renderDevicePicker()}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
</footer>`}`;
${this.selectedDeviceChallenge
? this.renderDeviceChallenge()
: html`<div class="pf-c-login__main-body">${this.renderDevicePicker()}</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`}`;
}
}

View File

@ -13,12 +13,18 @@ import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import "../../FormStatic";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
export class AuthenticatorValidateStageWebCode extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@ -31,60 +37,72 @@ export class AuthenticatorValidateStageWebCode extends BaseStage<AuthenticatorVa
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<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)}">${t`Not you?`}</a>
</div>
</ak-form-static>
<ak-form-element
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}>
<!-- @ts-ignore -->
<input type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
placeholder="${t`Please enter your TOTP Code`}"
autofocus=""
autocomplete="one-time-code"
class="pf-c-form-control"
value="${PasswordManagerPrefill.totp || ""}"
required>
</ak-form-element>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<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)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<ak-form-element
label="${t`Code`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["code"]}
>
<!-- @ts-ignore -->
<input
type="text"
name="code"
inputmode="numeric"
pattern="[0-9]*"
placeholder="${t`Please enter your TOTP 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">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</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;
}}>
${t`Return to device picker`}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</li>`:
html``}
</ul>
</footer>`;
</div>
</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;
}}
>
${t`Return to device picker`}
</button>
</li>`
: html``}
</ul>
</footer>`;
}
}

View File

@ -12,12 +12,18 @@ import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import "../../FormStatic";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-validate-duo")
export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
export class AuthenticatorValidateStageWebDuo extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@ -30,49 +36,58 @@ export class AuthenticatorValidateStageWebDuo extends BaseStage<AuthenticatorVal
firstUpdated(): void {
this.host?.submit({
"duo": this.deviceChallenge?.deviceUid
duo: this.deviceChallenge?.deviceUid,
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
}
return html`<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<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)}">${t`Not you?`}</a>
</div>
</ak-form-static>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<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)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</div>
</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;
}}>
${t`Return to device picker`}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
</button>
</li>`:
html``}
</ul>
</footer>`;
</div>
</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;
}}
>
${t`Return to device picker`}
</button>
</li>`
: html``}
</ul>
</footer>`;
}
}

View File

@ -8,15 +8,24 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { PFSize } from "../../../elements/Spinner";
import { transformAssertionForServer, transformCredentialRequestOptions } from "../authenticator_webauthn/utils";
import {
transformAssertionForServer,
transformCredentialRequestOptions,
} from "../authenticator_webauthn/utils";
import { BaseStage } from "../base";
import { AuthenticatorValidateStage } from "./AuthenticatorValidateStage";
import { AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
import {
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest,
DeviceChallenge,
} from "authentik-api";
@customElement("ak-stage-authenticator-validate-webauthn")
export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorValidationChallenge, AuthenticatorValidationChallengeResponseRequest> {
@property({attribute: false})
export class AuthenticatorValidateStageWebAuthn extends BaseStage<
AuthenticatorValidationChallenge,
AuthenticatorValidationChallengeResponseRequest
> {
@property({ attribute: false })
deviceChallenge?: DeviceChallenge;
@property({ type: Boolean })
@ -25,7 +34,7 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
@property()
authenticateMessage = "";
@property({type: Boolean})
@property({ type: Boolean })
showBackButton = false;
static get styles(): CSSResult[] {
@ -35,8 +44,11 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
async authenticate(): Promise<void> {
// convert certain members of the PublicKeyCredentialRequestOptions into
// byte arrays as expected by the spec.
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>this.deviceChallenge?.challenge;
const transformedCredentialRequestOptions = transformCredentialRequestOptions(credentialRequestOptions);
const credentialRequestOptions = <PublicKeyCredentialRequestOptions>(
this.deviceChallenge?.challenge
);
const transformedCredentialRequestOptions =
transformCredentialRequestOptions(credentialRequestOptions);
// request the authenticator to create an assertion signature using the
// credential private key
@ -54,12 +66,14 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
// 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(<PublicKeyCredential>assertion);
const transformedAssertionForServer = transformAssertionForServer(
<PublicKeyCredential>assertion,
);
// post the assertion to the server for verification.
try {
await this.host?.submit({
webauthn: transformedAssertionForServer
webauthn: transformedAssertionForServer,
});
} catch (err) {
throw new Error(t`Error when validating assertion on server: ${err}`);
@ -75,48 +89,56 @@ export class AuthenticatorValidateStageWebAuthn extends BaseStage<AuthenticatorV
return;
}
this.authenticateRunning = true;
this.authenticate().catch((e) => {
console.error(e);
this.authenticateMessage = e.toString();
}).finally(() => {
this.authenticateRunning = false;
});
this.authenticate()
.catch((e) => {
console.error(e);
this.authenticateMessage = e.toString();
})
.finally(() => {
this.authenticateRunning = false;
});
}
render(): TemplateResult {
return html`<div class="pf-c-login__main-body">
${this.authenticateRunning ?
html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`:
html`
<div class="pf-c-form__group pf-m-action">
<p class="pf-m-block">${this.authenticateMessage}</p>
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.authenticateWrapper();
}}>
${t`Retry authentication`}
</button>
</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;
}}>
${t`Return to device picker`}
</button>
</li>`:
html``}
</ul>
</footer>`;
${this.authenticateRunning
? html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`
: html` <div class="pf-c-form__group pf-m-action">
<p class="pf-m-block">${this.authenticateMessage}</p>
<button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.authenticateWrapper();
}}
>
${t`Retry authentication`}
</button>
</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;
}}
>
${t`Return to device picker`}
</button>
</li>`
: html``}
</ul>
</footer>`;
}
}

View File

@ -9,17 +9,26 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
import AKGlobal from "../../../authentik.css";
import { PFSize } from "../../../elements/Spinner";
import { BaseStage } from "../base";
import { Assertion, transformCredentialCreateOptions, transformNewAssertionForServer } from "./utils";
import { AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest } from "authentik-api";
import {
Assertion,
transformCredentialCreateOptions,
transformNewAssertionForServer,
} from "./utils";
import {
AuthenticatorWebAuthnChallenge,
AuthenticatorWebAuthnChallengeResponseRequest,
} from "authentik-api";
export interface WebAuthnAuthenticatorRegisterChallengeResponse {
response: Assertion;
}
@customElement("ak-stage-authenticator-webauthn")
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorWebAuthnChallenge, AuthenticatorWebAuthnChallengeResponseRequest> {
@property({type: Boolean})
export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
AuthenticatorWebAuthnChallenge,
AuthenticatorWebAuthnChallengeResponseRequest
> {
@property({ type: Boolean })
registerRunning = false;
@property()
@ -35,13 +44,15 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
}
// convert certain members of the PublicKeyCredentialCreateOptions into
// byte arrays as expected by the spec.
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(this.challenge?.registration as PublicKeyCredentialCreationOptions);
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
this.challenge?.registration as PublicKeyCredentialCreationOptions,
);
// request the authenticator(s) to create a new credential keypair.
let credential;
try {
credential = <PublicKeyCredential> await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions
credential = <PublicKeyCredential>await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions,
});
if (!credential) {
throw new Error("Credential is empty");
@ -58,7 +69,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
// and storing the public key
try {
await this.host?.submit({
response: newAssertionForServer
response: newAssertionForServer,
});
} catch (err) {
throw new Error(t`Server validation of credential failed: ${err}`);
@ -70,12 +81,14 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
return;
}
this.registerRunning = true;
this.register().catch((e) => {
console.error(e);
this.registerMessage = e.toString();
}).finally(() => {
this.registerRunning = false;
});
this.register()
.catch((e) => {
console.error(e);
this.registerMessage = e.toString();
})
.finally(() => {
this.registerRunning = false;
});
}
firstUpdated(): void {
@ -89,26 +102,32 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
</h1>
</header>
<div class="pf-c-login__main-body">
${this.registerRunning ?
html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`:
html`
<div class="pf-c-form__group pf-m-action">
${this.challenge?.responseErrors ?
html`<p class="pf-m-block">${this.challenge.responseErrors["response"][0].string}</p>`:
html``}
<p class="pf-m-block">${this.registerMessage}</p>
<button class="pf-c-button pf-m-primary pf-m-block" @click=${() => {
this.registerWrapper();
}}>
${t`Register device`}
</button>
</div>`}
${
this.registerRunning
? html`<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${PFSize.XLarge}"></ak-spinner>
</div>
</div>
</div>`
: html` <div class="pf-c-form__group pf-m-action">
${this.challenge?.responseErrors
? html`<p class="pf-m-block">
${this.challenge.responseErrors["response"][0].string}
</p>`
: html``}
<p class="pf-m-block">${this.registerMessage}</p>
<button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
this.registerWrapper();
}}
>
${t`Register device`}
</button>
</div>`
}
</div>
</div>
<footer class="pf-c-login__main-footer">
@ -116,5 +135,4 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<AuthenticatorW
</ul>
</footer>`;
}
}

View File

@ -2,30 +2,28 @@ import * as base64js from "base64-js";
import { hexEncode } from "../../../utils";
export function b64enc(buf: Uint8Array): string {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}
export function b64RawEnc(buf: Uint8Array): string {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_");
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
*/
export function transformCredentialCreateOptions(credentialCreateOptions: PublicKeyCredentialCreationOptions): PublicKeyCredentialCreationOptions {
export function transformCredentialCreateOptions(
credentialCreateOptions: PublicKeyCredentialCreationOptions,
): PublicKeyCredentialCreationOptions {
const user = credentialCreateOptions.user;
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
const challenge = u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptions,
{ challenge, user });
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
challenge,
user,
});
return transformedCredentialCreateOptions;
}
@ -46,11 +44,10 @@ export interface Assertion {
*/
export function transformNewAssertionForServer(newAssertion: PublicKeyCredential): Assertion {
const attObj = new Uint8Array(
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject);
const clientDataJSON = new Uint8Array(
newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(
newAssertion.rawId);
(<AuthenticatorAttestationResponse>newAssertion.response).attestationObject,
);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
@ -59,26 +56,32 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
type: newAssertion.type,
attObj: b64enc(attObj),
clientData: b64enc(clientDataJSON),
registrationClientExtensions: JSON.stringify(registrationClientExtensions)
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
};
}
function u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), c => c.charCodeAt(0));
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
export function transformCredentialRequestOptions(credentialRequestOptions: PublicKeyCredentialRequestOptions): PublicKeyCredentialRequestOptions {
export function transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {
const challenge = u8arr(credentialRequestOptions.challenge.toString());
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(credentialDescriptor => {
const id = u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
});
const allowCredentials = (credentialRequestOptions.allowCredentials || []).map(
(credentialDescriptor) => {
const id = u8arr(credentialDescriptor.id.toString());
return Object.assign({}, credentialDescriptor, { id });
},
);
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptions,
{ challenge, allowCredentials });
const transformedCredentialRequestOptions = Object.assign({}, credentialRequestOptions, {
challenge,
allowCredentials,
});
return transformedCredentialRequestOptions;
}
@ -97,8 +100,8 @@ export interface AuthAssertion {
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion{
const response = <AuthenticatorAssertionResponse> newAssertion.response;
export function transformAssertionForServer(newAssertion: PublicKeyCredential): AuthAssertion {
const response = <AuthenticatorAssertionResponse>newAssertion.response;
const authData = new Uint8Array(response.authenticatorData);
const clientDataJSON = new Uint8Array(response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
@ -112,6 +115,6 @@ export function transformAssertionForServer(newAssertion: PublicKeyCredential):
authData: b64RawEnc(authData),
clientData: b64RawEnc(clientDataJSON),
signature: hexEncode(sig),
assertionClientExtensions: JSON.stringify(assertionClientExtensions)
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
};
}

View File

@ -12,36 +12,37 @@ import "../../../elements/EmptyState";
import { AutosubmitChallenge, AutoSubmitChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-autosubmit")
export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitChallengeResponseRequest> {
export class AutosubmitStage extends BaseStage<
AutosubmitChallenge,
AutoSubmitChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
updated(): void {
this.shadowRoot?.querySelectorAll("form").forEach((form) => {form.submit();});
this.shadowRoot?.querySelectorAll("form").forEach((form) => {
form.submit();
});
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" action="${this.challenge.url}" method="POST">
${Object.entries(this.challenge.attrs).map(([ key, value ]) => {
return html`<input type="hidden" name="${key as string}" value="${value as string}">`;
${Object.entries(this.challenge.attrs).map(([key, value]) => {
return html`<input
type="hidden"
name="${key as string}"
value="${value as string}"
/>`;
})}
<ak-empty-state
?loading="${true}">
</ak-empty-state>
<ak-empty-state ?loading="${true}"> </ak-empty-state>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
@ -50,9 +51,7 @@ export class AutosubmitStage extends BaseStage<AutosubmitChallenge, AutoSubmitCh
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -7,7 +7,6 @@ export interface StageHost {
}
export class BaseStage<Tin, Tout> extends LitElement {
host!: StageHost;
@property({ attribute: false })
@ -19,7 +18,7 @@ export class BaseStage<Tin, Tout> extends LitElement {
[key: string]: unknown;
} = {};
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 as unknown as Tout);
}
@ -28,18 +27,14 @@ export class BaseStage<Tin, Tout> extends LitElement {
return html``;
}
return html`<div class="pf-c-form__alert">
${errors.map(err => {
${errors.map((err) => {
return html`<div class="pf-c-alert pf-m-inline pf-m-danger">
<div class="pf-c-alert__icon">
<i class="fas fa-exclamation-circle"></i>
</div>
<h4 class="pf-c-alert__title">
${err.string}
</h4>
<h4 class="pf-c-alert__title">${err.string}</h4>
</div>`;
})}
</div>`;
}
}

View File

@ -17,7 +17,6 @@ import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-captcha")
export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
@ -38,7 +37,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
sitekey: this.challenge.siteKey,
callback: (token) => {
this.host?.submit({
"token": token,
token: token,
});
},
size: "invisible",
@ -51,24 +50,22 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="ak-loading">
@ -77,9 +74,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -15,48 +15,63 @@ import "../../FormStatic";
import { ConsentChallenge, ConsentChallengeResponseRequest } from "authentik-api";
import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-consent")
export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFList, PFForm, PFSpacing, PFFormControl, PFTitle, PFButton, AKGlobal];
return [
PFBase,
PFLogin,
PFList,
PFForm,
PFSpacing,
PFFormControl,
PFTitle,
PFButton,
AKGlobal,
];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<div class="pf-c-form__group">
<p id="header-text" class="pf-u-mb-xl">
${this.challenge.headerText}
</p>
${this.challenge.permissions.length > 0 ? html`
<p class="pf-u-mb-sm">${t`Application requires following permissions:`}</p>
<ul class="pf-c-list" id="permmissions">
${this.challenge.permissions.map((permission) => {
return html`<li data-permission-code="${permission.id}">${permission.name}</li>`;
})}
</ul>
` : html``}
<p id="header-text" class="pf-u-mb-xl">${this.challenge.headerText}</p>
${this.challenge.permissions.length > 0
? html`
<p class="pf-u-mb-sm">
${t`Application requires following permissions:`}
</p>
<ul class="pf-c-list" id="permmissions">
${this.challenge.permissions.map((permission) => {
return html`<li data-permission-code="${permission.id}">
${permission.name}
</li>`;
})}
</ul>
`
: html``}
</div>
<div class="pf-c-form__group pf-m-action">
@ -67,9 +82,7 @@ export class ConsentStage extends BaseStage<ConsentChallenge, ConsentChallengeRe
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -14,25 +14,24 @@ import { DummyChallenge, DummyChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-dummy")
export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
@ -41,9 +40,7 @@ export class DummyStage extends BaseStage<DummyChallenge, DummyChallengeResponse
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -13,29 +13,26 @@ import { EmailChallenge, EmailChallengeResponseRequest } from "authentik-api";
@customElement("ak-stage-email")
export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => { this.submitForm(e); }}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<div class="pf-c-form__group">
<p>
${t`Check your Emails for a password reset link.`}
</p>
<p>${t`Check your Emails for a password reset link.`}</p>
</div>
<div class="pf-c-form__group pf-m-action">
@ -46,9 +43,7 @@ export class EmailStage extends BaseStage<EmailChallenge, EmailChallengeResponse
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -11,7 +11,12 @@ import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import AKGlobal from "../../../authentik.css";
import "../../../elements/forms/FormElement";
import "../../../elements/EmptyState";
import { IdentificationChallenge, IdentificationChallengeResponseRequest, LoginSource, UserFieldsEnum } from "authentik-api";
import {
IdentificationChallenge,
IdentificationChallengeResponseRequest,
LoginSource,
UserFieldsEnum,
} from "authentik-api";
export const PasswordManagerPrefill: {
password: string | undefined;
@ -21,13 +26,27 @@ export const PasswordManagerPrefill: {
totp: undefined,
};
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", { style: "short", type: "disjunction" });
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", {
style: "short",
type: "disjunction",
});
@customElement("ak-stage-identification")
export class IdentificationStage extends BaseStage<IdentificationChallenge, IdentificationChallengeResponseRequest> {
export class IdentificationStage extends BaseStage<
IdentificationChallenge,
IdentificationChallengeResponseRequest
> {
static get styles(): CSSResult[] {
return [PFBase, PFAlert, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal].concat(
return [
PFBase,
PFAlert,
PFLogin,
PFForm,
PFFormControl,
PFTitle,
PFButton,
AKGlobal,
].concat(
css`
/* login page's icons */
.pf-c-login__main-footer-links-item button {
@ -41,7 +60,7 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
height: 100%;
max-height: var(--pf-c-login__main-footer-links-item-link-svg--Height);
}
`
`,
);
}
@ -56,12 +75,14 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
username.setAttribute("autocomplete", "username");
username.onkeyup = (ev: Event) => {
const el = ev.target as HTMLInputElement;
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
input.value = el.value;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
input.value = el.value;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
wrapperForm.appendChild(username);
const password = document.createElement("input");
@ -79,11 +100,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
PasswordManagerPrefill.password = el.value;
// Because password managers fill username, then password,
// we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
wrapperForm.appendChild(password);
const totp = document.createElement("input");
@ -101,11 +124,13 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
PasswordManagerPrefill.totp = el.value;
// Because totp managers fill username, then password, then optionally,
// we need to re-focus the uid_field here too
(this.shadowRoot || this).querySelectorAll<HTMLInputElement>("input[name=uidField]").forEach(input => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
wrapperForm.appendChild(totp);
}
@ -113,16 +138,19 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
renderSource(source: LoginSource): TemplateResult {
let icon = html`<i class="fas fas fa-share-square" title="${source.name}"></i>`;
if (source.iconUrl) {
icon = html`<img src="${source.iconUrl}" alt="${source.name}">`;
icon = html`<img src="${source.iconUrl}" alt="${source.name}" />`;
}
return html`<li class="pf-c-login__main-footer-links-item">
<button type="button" @click=${() => {
<button
type="button"
@click=${() => {
if (!this.host) return;
this.host.challenge = source.challenge;
}}>
${icon}
</button>
</li>`;
}}
>
${icon}
</button>
</li>`;
}
renderFooter(): TemplateResult {
@ -130,24 +158,26 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
return html``;
}
return html`<div class="pf-c-login__main-footer-band">
${this.challenge.enrollUrl ? html`
<p class="pf-c-login__main-footer-band-item">
${t`Need an account?`}
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
</p>` : html``}
${this.challenge.recoveryUrl ? html`
<p class="pf-c-login__main-footer-band-item">
<a id="recovery" href="${this.challenge.recoveryUrl}">${t`Forgot username or password?`}</a>
</p>` : html``}
</div>`;
${this.challenge.enrollUrl
? html` <p class="pf-c-login__main-footer-band-item">
${t`Need an account?`}
<a id="enroll" href="${this.challenge.enrollUrl}">${t`Sign up.`}</a>
</p>`
: html``}
${this.challenge.recoveryUrl
? html` <p class="pf-c-login__main-footer-band-item">
<a id="recovery" href="${this.challenge.recoveryUrl}"
>${t`Forgot username or password?`}</a
>
</p>`
: html``}
</div>`;
}
renderInput(): TemplateResult {
let type = "text";
if (!this.challenge?.userFields) {
return html`<p>
${t`Select one of the sources below to login.`}
</p>`;
return html`<p>${t`Select one of the sources below to login.`}</p>`;
}
const fields = (this.challenge?.userFields || []).sort();
// Check if the field should be *only* email to set the input type
@ -159,40 +189,48 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
[UserFieldsEnum.Email]: t`Email`,
[UserFieldsEnum.Upn]: t`UPN`,
};
const label = OR_LIST_FORMATTERS.format(fields.map(f => uiFields[f]));
const label = OR_LIST_FORMATTERS.format(fields.map((f) => uiFields[f]));
return html`<ak-form-element
label=${label}
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["uid_field"]}>
.errors=${(this.challenge.responseErrors || {})["uid_field"]}
>
<!-- @ts-ignore -->
<input type=${type}
<input
type=${type}
name="uidField"
placeholder=${label}
autofocus=""
autocomplete="username"
class="pf-c-form-control"
required>
required
/>
</ak-form-element>
${this.challenge.passwordFields ? html`
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}>
<input type="password"
name="password"
placeholder="${t`Password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}>
</ak-form-element>
`: html``}
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []) :
html``}
${this.challenge.passwordFields
? html`
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge.responseErrors || {})["password"]}
>
<input
type="password"
name="password"
placeholder="${t`Password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}
/>
</ak-form-element>
`
: html``}
${"non_field_errors" in (this.challenge?.responseErrors || {})
? this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || [])
: html``}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${this.challenge.primaryAction}
@ -202,23 +240,21 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
${this.challenge.applicationPre ?
html`<p>
${t`Login to continue to ${this.challenge.applicationPre}.`}
</p>`:
html``}
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.challenge.applicationPre
? html`<p>${t`Login to continue to ${this.challenge.applicationPre}.`}</p>`
: html``}
${this.renderInput()}
</form>
</div>
@ -231,5 +267,4 @@ export class IdentificationStage extends BaseStage<IdentificationChallenge, Iden
${this.renderFooter()}
</footer>`;
}
}

View File

@ -17,52 +17,62 @@ import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-password")
export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
<ak-form-static
class="pf-c-form__group"
userAvatar="${this.challenge.pendingUserAvatar}"
user=${this.challenge.pendingUser}>
user=${this.challenge.pendingUser}
>
<div slot="link">
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}">${t`Not you?`}</a>
<a href="${ifDefined(this.challenge.flowInfo?.cancelUrl)}"
>${t`Not you?`}</a
>
</div>
</ak-form-static>
<input name="username" autocomplete="username" type="hidden" value="${this.challenge.pendingUser}">
<input
name="username"
autocomplete="username"
type="hidden"
value="${this.challenge.pendingUser}"
/>
<ak-form-element
label="${t`Password`}"
?required="${true}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})["password"]}>
<input type="password"
.errors=${(this.challenge?.responseErrors || {})["password"]}
>
<input
type="password"
name="password"
placeholder="${t`Please enter your password`}"
autofocus=""
autocomplete="current-password"
class="pf-c-form-control"
required
value=${PasswordManagerPrefill.password || ""}>
value=${PasswordManagerPrefill.password || ""}
/>
</ak-form-element>
${this.challenge.recoveryUrl ?
html`<a href="${this.challenge.recoveryUrl}">
${t`Forgot password?`}</a>` : ""}
${this.challenge.recoveryUrl
? html`<a href="${this.challenge.recoveryUrl}"> ${t`Forgot password?`}</a>`
: ""}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
@ -72,9 +82,7 @@ export class PasswordStage extends BaseStage<PasswordChallenge, PasswordChalleng
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

View File

@ -15,10 +15,8 @@ import "../../../elements/EmptyState";
import "../../../elements/Divider";
import { PromptChallenge, PromptChallengeResponseRequest, StagePrompt } from "authentik-api";
@customElement("ak-stage-prompt")
export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeResponseRequest> {
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFAlert, PFForm, PFFormControl, PFTitle, PFButton, AKGlobal];
}
@ -104,34 +102,41 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state
?loading="${true}"
header=${t`Loading`}>
</ak-empty-state>`;
return html`<ak-empty-state ?loading="${true}" header=${t`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>
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form" @submit=${(e: Event) => {this.submitForm(e);}}>
<form
class="pf-c-form"
@submit=${(e: Event) => {
this.submitForm(e);
}}
>
${this.challenge.fields.map((prompt) => {
// Special types that aren't rendered in a wrapper
if (prompt.type === "static" || prompt.type === "hidden" || prompt.type === "separator") {
if (
prompt.type === "static" ||
prompt.type === "hidden" ||
prompt.type === "separator"
) {
return unsafeHTML(this.renderPromptInner(prompt));
}
return html`<ak-form-element
label="${prompt.label}"
?required="${prompt.required}"
class="pf-c-form__group"
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}>
.errors=${(this.challenge?.responseErrors || {})[prompt.fieldKey]}
>
${unsafeHTML(this.renderPromptInner(prompt))}
</ak-form-element>`;
})}
${"non_field_errors" in (this.challenge?.responseErrors || {}) ?
this.renderNonFieldErrors(this.challenge?.responseErrors?.non_field_errors || []):
html``}
${"non_field_errors" in (this.challenge?.responseErrors || {})
? this.renderNonFieldErrors(
this.challenge?.responseErrors?.non_field_errors || [],
)
: html``}
<div class="pf-c-form__group pf-m-action">
<button type="submit" class="pf-c-button pf-m-primary pf-m-block">
${t`Continue`}
@ -140,9 +145,7 @@ export class PromptStage extends BaseStage<PromptChallenge, PromptChallengeRespo
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links">
</ul>
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}