Merge branch 'main' into web/update-provider-forms-for-invalidation

* main: (22 commits)
  lifecycle: fix missing krb5 deps for full testing in image (#11815)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11810)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11809)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11808)
  web: bump API Client version (#11807)
  core: bump goauthentik.io/api/v3 from 3.2024083.12 to 3.2024083.13 (#11806)
  core: bump ruff from 0.7.0 to 0.7.1 (#11805)
  core: bump twilio from 9.3.4 to 9.3.5 (#11804)
  core, web: update translations (#11803)
  providers/scim: handle no members in group in consistency check (#11801)
  stages/identification: add captcha to identification stage (#11711)
  website/docs: improve root page and redirect (#11798)
  providers/scim: clamp batch size for patch requests (#11797)
  web/admin: fix missing div in wizard forms (#11794)
  providers/proxy: fix handling of AUTHENTIK_HOST_BROWSER (#11722)
  core, web: update translations (#11789)
  core: bump goauthentik.io/api/v3 from 3.2024083.11 to 3.2024083.12 (#11790)
  core: bump gssapi from 1.8.3 to 1.9.0 (#11791)
  web: bump API Client version (#11792)
  stages/authenticator_validate: autoselect last used 2fa device (#11087)
  ...
This commit is contained in:
Ken Sternberg
2024-10-28 09:37:16 -07:00
68 changed files with 1704 additions and 1345 deletions

View File

@ -29,7 +29,7 @@ export class ApplicationWizardPageBase
return AwadStyles;
}
@consume({ context: applicationWizardContext })
@consume({ context: applicationWizardContext, subscribe: true })
public wizard!: ApplicationWizardState;
@query("form")

View File

@ -1,7 +1,12 @@
import { createContext } from "@lit/context";
import { LocalTypeCreate } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
import { ApplicationWizardState } from "./types";
export const applicationWizardContext = createContext<ApplicationWizardState>(
Symbol("ak-application-wizard-state-context"),
);
export const applicationWizardProvidersContext = createContext<LocalTypeCreate[]>(
Symbol("ak-application-wizard-providers-context"),
);

View File

@ -1,3 +1,4 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { AkWizard } from "@goauthentik/components/ak-wizard-main/AkWizard";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
@ -5,7 +6,10 @@ import { ContextProvider } from "@lit/context";
import { msg } from "@lit/localize";
import { customElement, state } from "lit/decorators.js";
import { applicationWizardContext } from "./ContextIdentity";
import { ProvidersApi, ProxyMode } from "@goauthentik/api";
import { applicationWizardContext, applicationWizardProvidersContext } from "./ContextIdentity";
import { providerTypeRenderers } from "./auth-method-choice/ak-application-wizard-authentication-method-choice.choices.js";
import { newSteps } from "./steps";
import {
ApplicationStep,
@ -19,6 +23,7 @@ const freshWizardState = (): ApplicationWizardState => ({
app: {},
provider: {},
errors: {},
proxyMode: ProxyMode.Proxy,
});
@customElement("ak-application-wizard")
@ -46,6 +51,11 @@ export class ApplicationWizard extends CustomListenerElement(
initialValue: this.wizardState,
});
wizardProviderProvider = new ContextProvider(this, {
context: applicationWizardProvidersContext,
initialValue: [],
});
/**
* One of our steps has multiple display variants, one for each type of service provider. We
* want to *preserve* a customer's decisions about different providers; never make someone "go
@ -56,6 +66,21 @@ export class ApplicationWizard extends CustomListenerElement(
*/
providerCache: Map<string, OneOfProvider> = new Map();
connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
const wizardReadyProviders = Object.keys(providerTypeRenderers);
this.wizardProviderProvider.setValue(
providerTypes
.filter((providerType) => wizardReadyProviders.includes(providerType.modelName))
.map((providerType) => ({
...providerType,
renderer: providerTypeRenderers[providerType.modelName],
})),
);
});
}
// And this is where all the special cases go...
handleUpdate(detail: ApplicationWizardStateUpdate) {
if (detail.status === "submitted") {

View File

@ -1,176 +1,28 @@
import "@goauthentik/admin/common/ak-license-notice";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
import type {
LDAPProviderRequest,
ModelRequest,
OAuth2ProviderRequest,
ProxyProviderRequest,
RACProviderRequest,
RadiusProviderRequest,
SAMLProviderRequest,
SCIMProviderRequest,
} from "@goauthentik/api";
import { OneOfProvider } from "../types";
import type { TypeCreate } from "@goauthentik/api";
type ProviderRenderer = () => TemplateResult;
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
type ProviderNoteProvider = () => TemplateResult | undefined;
type ProviderNote = ProviderNoteProvider | undefined;
export type LocalTypeCreate = TypeCreate & {
formName: string;
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
renderer: ProviderRenderer;
};
export const providerModelsList: LocalTypeCreate[] = [
{
formName: "oauth2provider",
name: msg("OAuth2/OpenID Provider"),
description: msg("Modern applications, APIs and Single-page applications."),
renderer: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
modelName: ProviderModelEnum.Oauth2Oauth2provider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.Oauth2Oauth2provider,
...(provider as OAuth2ProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/openidconnect.svg",
},
{
formName: "ldapprovider",
name: msg("LDAP Provider"),
description: msg(
"Provide an LDAP interface for applications and users to authenticate against.",
),
renderer: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
modelName: ProviderModelEnum.LdapLdapprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.LdapLdapprovider,
...(provider as LDAPProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/ldap.png",
},
{
formName: "proxyprovider-proxy",
name: msg("Transparent Reverse Proxy"),
description: msg("For transparent reverse proxies with required authentication"),
renderer: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.Proxy,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwardsingle",
name: msg("Forward Auth (Single Application)"),
description: msg("For nginx's auth_request or traefik's forwardAuth"),
renderer: () =>
html`<ak-application-wizard-authentication-for-single-forward-proxy></ak-application-wizard-authentication-for-single-forward-proxy>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardSingle,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "proxyprovider-forwarddomain",
name: msg("Forward Auth (Domain Level)"),
description: msg("For nginx's auth_request or traefik's forwardAuth per root domain"),
renderer: () =>
html`<ak-application-wizard-authentication-for-forward-proxy-domain></ak-application-wizard-authentication-for-forward-proxy-domain>`,
modelName: ProviderModelEnum.ProxyProxyprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ProxyProxyprovider,
...(provider as ProxyProviderRequest),
mode: ProxyMode.ForwardDomain,
}),
component: "",
iconUrl: "/static/authentik/sources/proxy.svg",
},
{
formName: "racprovider",
name: msg("Remote Access Provider"),
description: msg("Remotely access computers/servers via RDP/SSH/VNC"),
renderer: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
modelName: ProviderModelEnum.RacRacprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RacRacprovider,
...(provider as RACProviderRequest),
}),
note: () => html`<ak-license-notice></ak-license-notice>`,
requiresEnterprise: true,
component: "",
iconUrl: "/static/authentik/sources/rac.svg",
},
{
formName: "samlprovider",
name: msg("SAML Provider"),
description: msg("Configure SAML provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
modelName: ProviderModelEnum.SamlSamlprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.SamlSamlprovider,
...(provider as SAMLProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/saml.png",
},
{
formName: "radiusprovider",
name: msg("Radius Provider"),
description: msg("Configure RADIUS provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
modelName: ProviderModelEnum.RadiusRadiusprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.RadiusRadiusprovider,
...(provider as RadiusProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/radius.svg",
},
{
formName: "scimprovider",
name: msg("SCIM Provider"),
description: msg("Configure SCIM provider manually"),
renderer: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
modelName: ProviderModelEnum.ScimScimprovider,
converter: (provider: OneOfProvider) => ({
providerModel: ProviderModelEnum.ScimScimprovider,
...(provider as SCIMProviderRequest),
}),
component: "",
iconUrl: "/static/authentik/sources/scim.png",
},
];
export const providerRendererList = new Map<string, ProviderRenderer>(
providerModelsList.map((tc) => [tc.formName, tc.renderer]),
);
export default providerModelsList;
export const providerTypeRenderers = {
oauth2provider: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
ldapprovider: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
proxyprovider: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
racprovider: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
samlprovider: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
radiusprovider: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
scimprovider: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
};

View File

@ -7,34 +7,29 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { TypeCreateWizardPageLayouts } from "@goauthentik/elements/wizard/TypeCreateWizardPage";
import { consume } from "@lit/context";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html } from "lit";
import BasePanel from "../BasePanel";
import { applicationWizardProvidersContext } from "../ContextIdentity";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import providerModelsList from "./ak-application-wizard-authentication-method-choice.choices";
@customElement("ak-application-wizard-authentication-method-choice")
export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSummary(BasePanel) {
@consume({ context: applicationWizardProvidersContext })
public providerModelsList: LocalTypeCreate[];
render() {
const selectedTypes = providerModelsList.filter(
(t) => t.formName === this.wizard.providerModel,
const selectedTypes = this.providerModelsList.filter(
(t) => t.modelName === this.wizard.providerModel,
);
// As a hack, the Application wizard has separate provider paths for our three types of
// proxy providers. This patch swaps the form we want to be directed to on page 3 from the
// modelName to the formName, so we get the right one. This information isn't modified
// or forwarded, so the proxy-plus-subtype is correctly mapped on submission.
const typesForWizard = providerModelsList.map((provider) => ({
...provider,
modelName: provider.formName,
}));
return providerModelsList.length > 0
return this.providerModelsList.length > 0
? html`<form class="pf-c-form pf-m-horizontal">
<ak-wizard-page-type-create
.types=${typesForWizard}
.types=${this.providerModelsList}
name="selectProviderType"
layout=${TypeCreateWizardPageLayouts.grid}
.selectedType=${selectedTypes.length > 0 ? selectedTypes[0] : undefined}
@ -42,7 +37,7 @@ export class ApplicationWizardAuthenticationMethodChoice extends WithLicenseSumm
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: ev.detail.formName,
providerModel: ev.detail.modelName,
errors: {},
},
status: this.valid ? "valid" : "invalid",

View File

@ -21,14 +21,14 @@ import PFBullseye from "@patternfly/patternfly/layouts/Bullseye/bullseye.css";
import {
type ApplicationRequest,
CoreApi,
type ModelRequest,
ProviderModelEnum,
ProxyMode,
type TransactionApplicationRequest,
type TransactionApplicationResponse,
ValidationError,
} from "@goauthentik/api";
import BasePanel from "../BasePanel";
import providerModelsList from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest {
return {
@ -38,14 +38,19 @@ function cleanApplication(app: Partial<ApplicationRequest>): ApplicationRequest
};
}
type ProviderModelType = Exclude<ModelRequest["providerModel"], "11184809">;
type State = {
state: "idle" | "running" | "error" | "success";
label: string | TemplateResult;
icon: string[];
};
const providerMap: Map<string, string> = Object.values(ProviderModelEnum)
.filter((value) => /^authentik_providers_/.test(value) && /provider$/.test(value))
.reduce((acc: Map<string, string>, value) => {
acc.set(value.split(".")[1], value);
return acc;
}, new Map());
const idleState: State = {
state: "idle",
label: "",
@ -98,19 +103,25 @@ export class ApplicationWizardCommitApplication extends BasePanel {
if (this.commitState === idleState) {
this.response = undefined;
this.commitState = runningState;
const providerModel = providerModelsList.find(
({ formName }) => formName === this.wizard.providerModel,
);
if (!providerModel) {
throw new Error(
`Could not determine provider model from user request: ${JSON.stringify(this.wizard, null, 2)}`,
);
// Stringly-based API. Not the best, but it works. Just be aware that it is
// stringly-based.
const providerModel = providerMap.get(this.wizard.providerModel);
const provider = this.wizard.provider;
provider.providerModel = providerModel;
// Special case for providers.
if (this.wizard.providerModel === "proxyprovider") {
provider.mode = this.wizard.proxyMode;
if (provider.model !== ProxyMode.ForwardDomain) {
provider.cookieDomain = "";
}
}
const request: TransactionApplicationRequest = {
providerModel: providerModel.modelName as ProviderModelType,
app: cleanApplication(this.wizard.app),
provider: providerModel.converter(this.wizard.provider),
providerModel,
provider,
};
this.send(request);

View File

@ -1,12 +1,12 @@
import { consume } from "@lit/context";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import BasePanel from "../BasePanel";
import { providerRendererList } from "../auth-method-choice/ak-application-wizard-authentication-method-choice.choices";
import { applicationWizardProvidersContext } from "../ContextIdentity";
import type { LocalTypeCreate } from "./ak-application-wizard-authentication-method-choice.choices";
import "./ldap/ak-application-wizard-authentication-by-ldap";
import "./oauth/ak-application-wizard-authentication-by-oauth";
import "./proxy/ak-application-wizard-authentication-for-forward-domain-proxy";
import "./proxy/ak-application-wizard-authentication-for-reverse-proxy";
import "./proxy/ak-application-wizard-authentication-for-single-forward-proxy";
import "./rac/ak-application-wizard-authentication-for-rac";
import "./radius/ak-application-wizard-authentication-by-radius";
import "./saml/ak-application-wizard-authentication-by-saml-configuration";
@ -14,14 +14,19 @@ import "./scim/ak-application-wizard-authentication-by-scim";
@customElement("ak-application-wizard-authentication-method")
export class ApplicationWizardApplicationDetails extends BasePanel {
@consume({ context: applicationWizardProvidersContext })
public providerModelsList: LocalTypeCreate[];
render() {
const handler = providerRendererList.get(this.wizard.providerModel);
const handler: LocalTypeCreate | undefined = this.providerModelsList.find(
({ modelName }) => modelName === this.wizard.providerModel,
);
if (!handler) {
throw new Error(
"Unrecognized authentication method in ak-application-wizard-authentication-method",
);
}
return handler();
return handler.renderer();
}
}

View File

@ -1,268 +0,0 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "@goauthentik/admin/providers/proxy/ProxyProviderPropertyMappings.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { state } from "@lit/reactive-element/decorators.js";
import { TemplateResult, html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PaginatedOAuthSourceList,
PaginatedScopeMappingList,
ProxyMode,
ProxyProvider,
SourcesApi,
} from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
type MaybeTemplateResult = TemplateResult | typeof nothing;
export class AkTypeProxyApplicationWizardPage extends BaseProviderPanel {
constructor() {
super();
new SourcesApi(DEFAULT_CONFIG)
.sourcesOauthList({
ordering: "name",
hasJwks: true,
})
.then((oauthSources: PaginatedOAuthSourceList) => {
this.oauthSources = oauthSources;
});
}
propertyMappings?: PaginatedScopeMappingList;
oauthSources?: PaginatedOAuthSourceList;
@state()
showHttpBasic = true;
@state()
mode: ProxyMode = ProxyMode.Proxy;
get instance(): ProxyProvider | undefined {
return this.wizard.provider as ProxyProvider;
}
renderModeDescription(): MaybeTemplateResult {
return nothing;
}
renderProxyMode(): TemplateResult {
throw new Error("Must be implemented in a child class.");
}
renderHttpBasic() {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
help=${msg(
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
)}
>
</ak-text-input>`;
}
render() {
const errors = this.wizard.errors.provider;
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${this.renderModeDescription()}
<ak-text-input
name="name"
value=${ifDefined(this.instance?.name)}
required
.errorMessages=${errors?.name ?? []}
label=${msg("Name")}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
${this.renderProxyMode()}
<ak-text-input
name="accessTokenValidity"
value=${first(this.instance?.accessTokenValidity, "hours=24")}
label=${msg("Token validity")}
help=${msg("Configure how long tokens are valid for.")}
.errorMessages=${errors?.accessTokenValidity ?? []}
></ak-text-input>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.certificate ?? undefined)}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-textarea-input
name="skipPathRegex"
label=${
this.mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")
}
value=${ifDefined(this.instance?.skipPathRegex)}
.errorMessages=${errors?.skipPathRegex ?? []}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>`}
>
</ak-textarea-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
label=${msg("Intercept header authentication")}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
></ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
label=${msg("Send HTTP-Basic Authentication")}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
></ak-switch-input>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}
export default AkTypeProxyApplicationWizardPage;

View File

@ -1,74 +0,0 @@
import "@goauthentik/components/ak-text-input";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import PFList from "@patternfly/patternfly/components/List/list.css";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-forward-proxy-domain")
export class AkForwardDomainProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
static get styles() {
return super.styles.concat(PFList);
}
renderModeDescription() {
return html`<p>
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div>
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>`;
}
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
return html`
<ak-text-input
name="externalHost"
label=${msg("External host")}
value=${ifDefined(provider?.externalHost)}
.errorMessages=${errors?.externalHost ?? []}
required
help=${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
>
</ak-text-input>
<ak-text-input
name="cookieDomain"
label=${msg("Cookie domain")}
value="${ifDefined(provider?.cookieDomain)}"
.errorMessages=${errors?.cookieDomain ?? []}
required
help=${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
></ak-text-input>
`;
}
}
export default AkForwardDomainProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-forward-proxy-domain": AkForwardDomainProxyApplicationWizardPage;
}
}

View File

@ -1,55 +1,46 @@
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import {
ProxyModeValue,
renderForm,
} from "@goauthentik/admin/providers/proxy/ProxyProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
import BaseProviderPanel from "../BaseProviderPanel.js";
@customElement("ak-application-wizard-authentication-for-reverse-proxy")
export class AkReverseProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
renderModeDescription() {
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>`;
}
export class AkReverseProxyApplicationWizardPage extends BaseProviderPanel {
@state()
showHttpBasic = true;
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
render() {
const onSetMode: SetMode = (ev: CustomEvent<ProxyModeValue>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
proxyMode: ev.detail.value,
},
});
// We deliberately chose not to make the forms "controlled," but we do need this form to
// respond immediately to a state change in the wizard.
window.setTimeout(() => this.requestUpdate(), 0);
};
return html` <ak-text-input
name="externalHost"
value=${ifDefined(provider?.externalHost)}
required
label=${msg("External host")}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>
<ak-text-input
name="internalHost"
value=${ifDefined(provider?.internalHost)}
.errorMessages=${errors?.internalHost ?? []}
required
label=${msg("Internal host")}
help=${msg("Upstream host that the requests are forwarded to.")}
></ak-text-input>
<ak-switch-input
name="internalHostSslValidation"
?checked=${first(provider?.internalHostSslValidation, true)}
label=${msg("Internal host SSL Validation")}
help=${msg("Validate SSL Certificates of upstream servers.")}
>
</ak-switch-input>`;
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
};
return html` <ak-wizard-title>${msg("Configure Proxy Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
${renderForm(this.wizard.provider ?? {}, this.wizard.errors.provider ?? [], {
mode: this.wizard.proxyMode,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,
})}
</form>`;
}
}

View File

@ -1,48 +0,0 @@
import "@goauthentik/components/ak-text-input";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators.js";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { ProxyProvider } from "@goauthentik/api";
import AkTypeProxyApplicationWizardPage from "./AuthenticationByProxyPage";
@customElement("ak-application-wizard-authentication-for-single-forward-proxy")
export class AkForwardSingleProxyApplicationWizardPage extends AkTypeProxyApplicationWizardPage {
renderModeDescription() {
return html`<p class="pf-u-mb-xl">
${msg(
html`Use this provider with nginx's <code>auth_request</code> or traefik's
<code>forwardAuth</code>. Each application/domain needs its own provider.
Additionally, on each domain, <code>/outpost.goauthentik.io</code> must be
routed to the outpost (when using a managed outpost, this is done for you).`,
)}
</p>`;
}
renderProxyMode() {
const provider = this.wizard.provider as ProxyProvider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-text-input
name="externalHost"
value=${ifDefined(provider?.externalHost)}
required
label=${msg("External host")}
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>`;
}
}
export default AkForwardSingleProxyApplicationWizardPage;
declare global {
interface HTMLElementTagNameMap {
"ak-application-wizard-authentication-for-single-forward-proxy": AkForwardSingleProxyApplicationWizardPage;
}
}

View File

@ -1,3 +1,5 @@
import { renderForm } from "@goauthentik/admin/providers/scim/SCIMProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html } from "lit";

View File

@ -5,6 +5,7 @@ import {
type LDAPProviderRequest,
type OAuth2ProviderRequest,
type ProvidersSamlImportMetadataCreateRequest,
ProxyMode,
type ProxyProviderRequest,
type RACProviderRequest,
type RadiusProviderRequest,
@ -27,6 +28,7 @@ export interface ApplicationWizardState {
providerModel: string;
app: Partial<ApplicationRequest>;
provider: OneOfProvider;
proxyMode?: ProxyMode;
errors: ValidationError;
}

View File

@ -43,8 +43,12 @@ export class ProviderWizard extends AKElement {
@query("ak-wizard")
wizard?: Wizard;
async firstUpdated(): Promise<void> {
this.providerTypes = await new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList();
connectedCallback() {
super.connectedCallback();
new ProvidersApi(DEFAULT_CONFIG).providersAllTypesList().then((providerTypes) => {
console.log(providerTypes);
this.providerTypes = providerTypes;
});
}
render(): TemplateResult {

View File

@ -1,39 +1,18 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, html } from "lit";
import { CSSResult } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFSpacing from "@patternfly/patternfly/utilities/Spacing/spacing.css";
import {
FlowsInstancesListDesignationEnum,
ProvidersApi,
ProxyMode,
ProxyProvider,
} from "@goauthentik/api";
import { ProvidersApi, ProxyMode, ProxyProvider } from "@goauthentik/api";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
import { SetMode, SetShowHttpBasic, renderForm } from "./ProxyProviderFormForm.js";
@customElement("ak-provider-proxy-form")
export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
@ -73,376 +52,22 @@ export class ProxyProviderFormPage extends BaseProviderForm<ProxyProvider> {
}
}
renderHttpBasic(): TemplateResult {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(this.instance?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(this.instance?.basicAuthPasswordAttribute)}"
help=${msg(
"User/Group Attribute used for the password part of the HTTP-Basic Header.",
)}
>
</ak-text-input>`;
}
renderModeSelector(): TemplateResult {
const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => {
renderForm() {
const onSetMode: SetMode = (ev) => {
this.mode = ev.detail.value;
};
// prettier-ignore
return html`
<ak-toggle-group value=${this.mode} @ak-toggle=${setMode} data-ouid-component-name="proxy-type-toggle">
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
</ak-toggle-group>
`;
}
const onSetShowHttpBasic: SetShowHttpBasic = (ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
};
renderSettings(): TemplateResult {
switch (this.mode) {
case ProxyMode.Proxy:
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>
<ak-form-element-horizontal
label=${msg("External host")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Internal host")}
?required=${true}
name="internalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Upstream host that the requests are forwarded to.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.internalHostSslValidation, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Internal host SSL Validation")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg("Validate SSL Certificates of upstream servers.")}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardSingle:
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).",
)}
</p>
<ak-form-element-horizontal
label=${msg("External host")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${ifDefined(this.instance?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.ForwardDomain:
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div class="pf-u-mb-xl">
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>
<ak-form-element-horizontal
label=${msg("Authentication URL")}
?required=${true}
name="externalHost"
>
<input
type="text"
value="${first(this.instance?.externalHost, window.location.origin)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Cookie domain")}
name="cookieDomain"
?required=${true}
>
<input
type="text"
value="${ifDefined(this.instance?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
</p>
</ak-form-element-horizontal>`;
case ProxyMode.UnknownDefaultOpenApi:
return html`<p>${msg("Unknown proxy mode")}</p>`;
}
}
renderForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${this.instance?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">${this.renderModeSelector()}</div>
<div class="pf-c-card__footer">${this.renderSettings()}</div>
</div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
<input
type="text"
value="${first(this.instance?.accessTokenValidity, "hours=24")}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">
${msg("Configure how long tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${this.instance?.certificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${this.mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">
${this.instance?.skipPathRegex}</textarea
>
<p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="interceptHeaderAuth">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Intercept header authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Send HTTP-Basic Authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
</p>
</ak-form-element-horizontal>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${false}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
return renderForm(this.instance ?? {}, [], {
mode: this.mode,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,
});
}
}

View File

@ -0,0 +1,381 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import "@goauthentik/components/ak-toggle-group";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { match } from "ts-pattern";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsInstancesListDesignationEnum, ProxyMode, ProxyProvider } from "@goauthentik/api";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
export type ProxyModeValue = { value: ProxyMode };
export type SetMode = (ev: CustomEvent<ProvxyModeValue>) => void;
export type SetShowHttpBasic = (ev: Event) => void;
export interface ProxyModeExtraArgs {
mode: ProxyMode;
onSetMode: SetMode;
showHttpBasic: boolean;
onSetShowHttpBasic: SetShowHttpBasic;
}
function renderHttpBasic(provider: ProxyProvider) {
return html`<ak-text-input
name="basicAuthUserAttribute"
label=${msg("HTTP-Basic Username Key")}
value="${ifDefined(provider?.basicAuthUserAttribute)}"
help=${msg(
"User/Group Attribute used for the user part of the HTTP-Basic Header. If not set, the user's Email address is used.",
)}
>
</ak-text-input>
<ak-text-input
name="basicAuthPasswordAttribute"
label=${msg("HTTP-Basic Password Key")}
value="${ifDefined(provider?.basicAuthPasswordAttribute)}"
help=${msg("User/Group Attribute used for the password part of the HTTP-Basic Header.")}
>
</ak-text-input>`;
}
function renderModeSelector(mode: ProxyMode, onSet: SetMode) {
// prettier-ignore
return html` <ak-toggle-group
value=${mode}
@ak-toggle=${onSet}
data-ouid-component-name="proxy-type-toggle"
>
<option value=${ProxyMode.Proxy}>${msg("Proxy")}</option>
<option value=${ProxyMode.ForwardSingle}>${msg("Forward auth (single application)")}</option>
<option value=${ProxyMode.ForwardDomain}>${msg("Forward auth (domain level)")}</option>
</ak-toggle-group>`;
}
function renderProxySettings(provider: ProxyProvider) {
return html`<p class="pf-u-mb-xl">
${msg(
"This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well.",
)}
</p>
<ak-form-element-horizontal label=${msg("External host")} required name="externalHost">
<input
type="text"
value="${ifDefined(provider?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Internal host")} required name="internalHost">
<input
type="text"
value="${ifDefined(provider?.internalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Upstream host that the requests are forwarded to.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="internalHostSslValidation">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.internalHostSslValidation ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label">${msg("Internal host SSL Validation")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg("Validate SSL Certificates of upstream servers.")}
</p>
</ak-form-element-horizontal>`;
}
function renderForwardSingleSettings(provider: ProxyProvider) {
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Each application/domain needs its own provider. Additionally, on each domain, /outpost.goauthentik.io must be routed to the outpost (when using a managed outpost, this is done for you).",
)}
</p>
<ak-form-element-horizontal label=${msg("External host")} required name="externalHost">
<input
type="text"
value="${ifDefined(provider?.externalHost)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
</p>
</ak-form-element-horizontal>`;
}
function renderForwardDomainSettings(provider: ProxyProvider) {
return html`<p class="pf-u-mb-xl">
${msg(
"Use this provider with nginx's auth_request or traefik's forwardAuth. Only a single provider is required per root domain. You can't do per-application authorization, but you don't have to create a provider for each application.",
)}
</p>
<div class="pf-u-mb-xl">
${msg("An example setup can look like this:")}
<ul class="pf-c-list">
<li>${msg("authentik running on auth.example.com")}</li>
<li>${msg("app1 running on app1.example.com")}</li>
</ul>
${msg(
"In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.",
)}
</div>
<ak-form-element-horizontal label=${msg("Authentication URL")} required name="externalHost">
<input
type="text"
value="${provider?.externalHost ?? window.location.origin}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The external URL you'll authenticate at. The authentik core server should be reachable under this URL.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Cookie domain")} name="cookieDomain" required>
<input
type="text"
value="${ifDefined(provider?.cookieDomain)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.",
)}
</p>
</ak-form-element-horizontal>`;
}
function renderSettings(provider: ProxyProvider, mode: ProxyMode) {
return match(mode)
.with(ProxyMode.Proxy, () => renderProxySettings(provider))
.with(ProxyMode.ForwardSingle, () => renderForwardSingleSettings(provider))
.with(ProxyMode.ForwardDomain, () => renderForwardDomainSettings(provider))
.exhaustive();
}
export function renderForm(
provider?: Partial<ProxyProvider>,
errors: ValidationError,
args: ProxyModeExtraArgs,
) {
const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args;
return html`
<ak-form-element-horizontal label=${msg("Name")} required name="name">
<input
type="text"
value="${ifDefined(provider?.name)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
required
name="authorizationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<div class="pf-c-card pf-m-selectable pf-m-selected">
<div class="pf-c-card__body">${renderModeSelector(mode, onSetMode)}</div>
<div class="pf-c-card__footer">${renderSettings(provider, mode)}</div>
</div>
<ak-form-element-horizontal label=${msg("Token validity")} name="accessTokenValidity">
<input
type="text"
value="${provider?.accessTokenValidity ?? "hours=24"}"
class="pf-c-form-control"
/>
<p class="pf-c-form__helper-text">${msg("Configure how long tokens are valid for.")}</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-group>
<span slot="header">${msg("Advanced protocol settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${provider?.certificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Additional scopes")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${proxyPropertyMappingsProvider}
.selector=${makeProxyPropertyMappingsSelector(provider?.propertyMappings)}
available-label="${msg("Available Scopes")}"
selected-label="${msg("Selected Scopes")}"
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Additional scope mappings, which are passed to the proxy.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label="${mode === ProxyMode.ForwardDomain
? msg("Unauthenticated URLs")
: msg("Unauthenticated Paths")}"
name="skipPathRegex"
>
<textarea class="pf-c-form-control">${provider?.skipPathRegex}</textarea>
<p class="pf-c-form__helper-text">
${msg(
"Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.",
)}
</p>
<p class="pf-c-form__helper-text">
${msg(
"When using proxy or forward auth (single application) mode, the requested URL Path is checked against the regular expressions. When using forward auth (domain mode), the full requested URL including scheme and host is matched against the regular expressions.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="interceptHeaderAuth">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.interceptHeaderAuth ?? true}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Intercept header authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="basicAuthEnabled">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${provider?.basicAuthEnabled ?? false}
@change=${onSetShowHttpBasic}
/>
<span class="pf-c-switch__toggle">
<span class="pf-c-switch__toggle-icon">
<i class="fas fa-check" aria-hidden="true"></i>
</span>
</span>
<span class="pf-c-switch__label"
>${msg("Send HTTP-Basic Authentication")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
</p>
</ak-form-element-horizontal>
${showHttpBasic ? renderHttpBasic(provider) : nothing}
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -350,14 +350,12 @@ export function renderForm(
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default relay state")}
?required=${true}
name="defaultRelayState"
>
<input
type="text"
value="${provider?.defaultRelayState || ""}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(

View File

@ -21,6 +21,7 @@ import {
SourcesApi,
Stage,
StagesApi,
StagesCaptchaListRequest,
StagesPasswordListRequest,
UserFieldsEnum,
} from "@goauthentik/api";
@ -140,19 +141,13 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
).stagesPasswordList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) => {
return groupBy(items, (stage) => stage.verboseNamePlural);
}}
.renderElement=${(stage: Stage): string => {
return stage.name;
}}
.value=${(stage: Stage | undefined): string | undefined => {
return stage?.pk;
}}
.selected=${(stage: Stage): boolean => {
return stage.pk === this.instance?.passwordStage;
}}
?blankable=${true}
.groupBy=${(items: Stage[]) =>
groupBy(items, (stage) => stage.verboseNamePlural)}
.renderElement=${(stage: Stage): string => stage.name}
.value=${(stage: Stage | undefined): string | undefined => stage?.pk}
.selected=${(stage: Stage): boolean =>
stage.pk === this.instance?.passwordStage}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
@ -161,6 +156,35 @@ export class IdentificationStageForm extends BaseStageForm<IdentificationStage>
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Captcha stage")} name="captchaStage">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Stage[]> => {
const args: StagesCaptchaListRequest = {
ordering: "name",
};
if (query !== undefined) {
args.search = query;
}
const stages = await new StagesApi(
DEFAULT_CONFIG,
).stagesCaptchaList(args);
return stages.results;
}}
.groupBy=${(items: Stage[]) =>
groupBy(items, (stage) => stage.verboseNamePlural)}
.renderElement=${(stage: Stage): string => stage.name}
.value=${(stage: Stage | undefined): string | undefined => stage?.pk}
.selected=${(stage: Stage): boolean =>
stage.pk === this.instance?.captchaStage}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"When set, adds functionality exactly like a Captcha stage, but baked into the Identification stage.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="caseInsensitiveMatching">
<label class="pf-c-switch">
<input

View File

@ -6,7 +6,7 @@ import { BaseStage, StageHost, SubmitOptions } from "@goauthentik/flow/stages/ba
import { PasswordManagerPrefill } from "@goauthentik/flow/stages/identification/IdentificationStage";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html, nothing } from "lit";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -25,6 +25,37 @@ import {
FlowsApi,
} from "@goauthentik/api";
const customCSS = css`
ul {
padding-top: 1rem;
}
ul > li:not(:last-child) {
padding-bottom: 1rem;
}
.authenticator-button {
display: flex;
align-items: center;
}
:host([theme="dark"]) .authenticator-button {
color: var(--ak-dark-foreground) !important;
}
i {
font-size: 1.5rem;
padding: 1rem 0;
width: 3rem;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
text-align: left;
}
.right > * {
height: 50%;
}
`;
@customElement("ak-stage-authenticator-validate")
export class AuthenticatorValidateStage
extends BaseStage<
@ -33,6 +64,10 @@ export class AuthenticatorValidateStage
>
implements StageHost
{
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton, customCSS];
}
flowSlug = "";
set loading(value: boolean) {
@ -47,14 +82,18 @@ export class AuthenticatorValidateStage
return this.host.brand;
}
@state()
_firstInitialized: boolean = false;
@state()
_selectedDeviceChallenge?: DeviceChallenge;
set selectedDeviceChallenge(value: DeviceChallenge | undefined) {
const previousChallenge = this._selectedDeviceChallenge;
this._selectedDeviceChallenge = value;
if (!value) return;
if (value === previousChallenge) return;
if (value === undefined || value === previousChallenge) {
return;
}
// 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.
new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolve({
@ -79,37 +118,39 @@ export class AuthenticatorValidateStage
return this.host?.submit(payload, options) || Promise.resolve();
}
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFTitle, PFButton].concat(css`
ul {
padding-top: 1rem;
}
ul > li:not(:last-child) {
padding-bottom: 1rem;
}
.authenticator-button {
display: flex;
align-items: center;
}
:host([theme="dark"]) .authenticator-button {
color: var(--ak-dark-foreground) !important;
}
i {
font-size: 1.5rem;
padding: 1rem 0;
width: 3rem;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
text-align: left;
}
.right > * {
height: 50%;
}
`);
willUpdate(_changed: PropertyValues<this>) {
if (this._firstInitialized || !this.challenge) {
return;
}
this._firstInitialized = true;
// If user only has a single device, autoselect that device.
if (this.challenge.deviceChallenges.length === 1) {
this.selectedDeviceChallenge = this.challenge.deviceChallenges[0];
return;
}
// If TOTP is allowed from the backend and we have a pre-filled value
// from the password manager, autoselect TOTP.
const totpChallenge = this.challenge.deviceChallenges.find(
(challenge) => challenge.deviceClass === DeviceClassesEnum.Totp,
);
if (PasswordManagerPrefill.totp && totpChallenge) {
console.debug(
"authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
);
this.selectedDeviceChallenge = totpChallenge;
return;
}
// If the last used device is not Static, autoselect that device.
const lastUsedChallenge = this.challenge.deviceChallenges
.filter((deviceChallenge) => deviceChallenge.lastUsed)
.sort((a, b) => b.lastUsed!.valueOf() - a.lastUsed!.valueOf())[0];
if (lastUsedChallenge && lastUsedChallenge.deviceClass !== DeviceClassesEnum.Static) {
this.selectedDeviceChallenge = lastUsedChallenge;
}
}
renderDevicePickerSingle(deviceChallenge: DeviceChallenge) {
@ -228,45 +269,28 @@ export class AuthenticatorValidateStage
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state 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];
}
// TOTP is a bit special, assuming that TOTP is allowed from the backend,
// and we have a pre-filled value from the password manager,
// directly set the the TOTP device Challenge as active.
const totpChallenge = this.challenge.deviceChallenges.find(
(challenge) => challenge.deviceClass === DeviceClassesEnum.Totp,
);
if (PasswordManagerPrefill.totp && totpChallenge) {
console.debug(
"authentik/stages/authenticator_validate: found prefill totp code, selecting totp challenge",
);
this.selectedDeviceChallenge = totpChallenge;
}
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
${this.selectedDeviceChallenge
? this.renderDeviceChallenge()
: html`<div class="pf-c-login__main-body">
<form class="pf-c-form">
${this.renderUserInfo()}
${this.selectedDeviceChallenge
? ""
: html`<p>${msg("Select an authentication method.")}</p>`}
${this.challenge.configurationStages.length > 0
? this.renderStagePicker()
: html``}
</form>
${this.renderDevicePicker()}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`}`;
return this.challenge
? html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${this.challenge.flowInfo?.title}</h1>
</header>
${this.selectedDeviceChallenge
? this.renderDeviceChallenge()
: html`<div class="pf-c-login__main-body">
<form class="pf-c-form">
${this.renderUserInfo()}
${this.selectedDeviceChallenge
? ""
: html`<p>${msg("Select an authentication method.")}</p>`}
${this.challenge.configurationStages.length > 0
? this.renderStagePicker()
: html``}
</form>
${this.renderDevicePicker()}
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`}`
: html`<ak-empty-state loading> </ak-empty-state>`;
}
}

View File

@ -31,6 +31,34 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
`);
}
deviceMessage(): string {
switch (this.deviceChallenge?.deviceClass) {
case DeviceClassesEnum.Sms:
return msg("A code has been sent to you via SMS.");
case DeviceClassesEnum.Totp:
return msg(
"Open your two-factor authenticator app to view your authentication code.",
);
case DeviceClassesEnum.Static:
return msg("Enter a one-time recovery code for this user.");
}
return msg("Enter the code from your authenticator device.");
}
deviceIcon(): string {
switch (this.deviceChallenge?.deviceClass) {
case DeviceClassesEnum.Sms:
return "fa-key";
case DeviceClassesEnum.Totp:
return "fa-mobile-alt";
case DeviceClassesEnum.Static:
return "fa-sticky-note";
}
return "fa-mobile-alt";
}
render(): TemplateResult {
if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`;
@ -44,19 +72,8 @@ export class AuthenticatorValidateStageWebCode extends BaseDeviceStage<
>
${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>`}
<i class="fa ${this.deviceIcon()}" aria-hidden="true"></i>
<p>${this.deviceMessage()}</p>
</div>
<ak-form-element
label="${this.deviceChallenge?.deviceClass === DeviceClassesEnum.Static

View File

@ -59,7 +59,7 @@ export class BaseDeviceStage<
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
}}
>
${msg("Return to device picker")}
${msg("Select another authentication method")}
</button>`;
}
}

View File

@ -6,8 +6,8 @@ import { BaseStage } from "@goauthentik/flow/stages/base";
import type { TurnstileObject } from "turnstile-types";
import { msg } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { CSSResult, PropertyValues, html } from "lit";
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";
@ -22,6 +22,7 @@ import { CaptchaChallenge, CaptchaChallengeResponseRequest } from "@goauthentik/
interface TurnstileWindow extends Window {
turnstile: TurnstileObject;
}
type TokenHandler = (token: string) => void;
const captchaContainerID = "captcha-container";
@ -45,6 +46,11 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
@state()
scriptElement?: HTMLScriptElement;
@property()
onTokenChange: TokenHandler = (token: string) => {
this.host.submit({ component: "ak-stage-captcha", token });
};
constructor() {
super();
this.captchaContainer = document.createElement("div");
@ -102,11 +108,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
grecaptcha.ready(() => {
const captchaId = grecaptcha.render(this.captchaContainer, {
sitekey: this.challenge.siteKey,
callback: (token) => {
this.host?.submit({
token: token,
});
},
callback: this.onTokenChange,
size: "invisible",
});
grecaptcha.execute(captchaId);
@ -122,12 +124,8 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
document.body.appendChild(this.captchaContainer);
const captchaId = hcaptcha.render(this.captchaContainer, {
sitekey: this.challenge.siteKey,
callback: this.onTokenChange,
size: "invisible",
callback: (token) => {
this.host?.submit({
token: token,
});
},
});
hcaptcha.execute(captchaId);
return true;
@ -141,16 +139,12 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
document.body.appendChild(this.captchaContainer);
(window as unknown as TurnstileWindow).turnstile.render(`#${captchaContainerID}`, {
sitekey: this.challenge.siteKey,
callback: (token) => {
this.host?.submit({
token: token,
});
},
callback: this.onTokenChange,
});
return true;
}
renderBody(): TemplateResult {
renderBody() {
if (this.error) {
return html`<ak-empty-state icon="fa-times" header=${this.error}> </ak-empty-state>`;
}
@ -160,7 +154,7 @@ export class CaptchaStage extends BaseStage<CaptchaChallenge, CaptchaChallengeRe
return html`<ak-empty-state loading header=${msg("Verifying...")}></ak-empty-state>`;
}
render(): TemplateResult {
render() {
if (!this.challenge) {
return html`<ak-empty-state loading> </ak-empty-state>`;
}

View File

@ -4,10 +4,11 @@ import "@goauthentik/elements/EmptyState";
import "@goauthentik/elements/forms/FormElement";
import "@goauthentik/flow/components/ak-flow-password-input.js";
import { BaseStage } from "@goauthentik/flow/stages/base";
import "@goauthentik/flow/stages/captcha/CaptchaStage";
import { msg, str } from "@lit/localize";
import { CSSResult, PropertyValues, TemplateResult, css, html, nothing } from "lit";
import { customElement } from "lit/decorators.js";
import { customElement, state } from "lit/decorators.js";
import PFAlert from "@patternfly/patternfly/components/Alert/alert.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
@ -46,6 +47,9 @@ export class IdentificationStage extends BaseStage<
> {
form?: HTMLFormElement;
@state()
captchaToken = "";
static get styles(): CSSResult[] {
return [
PFBase,
@ -274,6 +278,18 @@ export class IdentificationStage extends BaseStage<
`
: nothing}
${this.renderNonFieldErrors()}
${this.challenge.captchaStage
? html`
<input name="captchaToken" type="hidden" .value="${this.captchaToken}" />
<ak-stage-captcha
style="visibility: hidden; position:absolute;"
.challenge=${this.challenge.captchaStage}
.onTokenChange=${(token: string) => {
this.captchaToken = token;
}}
></ak-stage-captcha>
`
: nothing}
<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}