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:
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.8.3-1729699127",
|
||||
"@goauthentik/api": "^2024.8.3-1729836831",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
@ -1775,9 +1775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@goauthentik/api": {
|
||||
"version": "2024.8.3-1729699127",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729699127.tgz",
|
||||
"integrity": "sha512-luo0SAASR6BTTtLszDgfdwofBejv4F3hCHgPxeSoTSFgE8/A2+zJD8EtWPZaa1udDkwPa9lbIeJSSmbgFke3jA=="
|
||||
"version": "2024.8.3-1729836831",
|
||||
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2024.8.3-1729836831.tgz",
|
||||
"integrity": "sha512-nOgvjYQiK+HhWuiZ635h/aSsq7Mfj5cDrIyBJt+IJRQuJFtnnHx8nscRXKK/8sBl9obH2zMCoZgeqytK8145bg=="
|
||||
},
|
||||
"node_modules/@goauthentik/web": {
|
||||
"resolved": "",
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
"@floating-ui/dom": "^1.6.11",
|
||||
"@formatjs/intl-listformat": "^7.5.7",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@goauthentik/api": "^2024.8.3-1729699127",
|
||||
"@goauthentik/api": "^2024.8.3-1729836831",
|
||||
"@lit-labs/ssr": "^3.2.2",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@lit/localize": "^0.12.2",
|
||||
|
||||
@ -29,7 +29,7 @@ export class ApplicationWizardPageBase
|
||||
return AwadStyles;
|
||||
}
|
||||
|
||||
@consume({ context: applicationWizardContext })
|
||||
@consume({ context: applicationWizardContext, subscribe: true })
|
||||
public wizard!: ApplicationWizardState;
|
||||
|
||||
@query("form")
|
||||
|
||||
@ -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"),
|
||||
);
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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>`,
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
381
web/src/admin/providers/proxy/ProxyProviderFormForm.ts
Normal file
381
web/src/admin/providers/proxy/ProxyProviderFormForm.ts
Normal 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>
|
||||
`;
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -59,7 +59,7 @@ export class BaseDeviceStage<
|
||||
(this.host as AuthenticatorValidateStage).selectedDeviceChallenge = undefined;
|
||||
}}
|
||||
>
|
||||
${msg("Return to device picker")}
|
||||
${msg("Select another authentication method")}
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>`;
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -89,7 +89,12 @@ export async function setFormGroup(name: string | RegExp, setting: "open" | "clo
|
||||
typeof name === "string" ? (sample) => sample === name : (sample) => name.test(sample);
|
||||
|
||||
const formGroup = await (async () => {
|
||||
for await (const group of $$("ak-form-group")) {
|
||||
for await (const group of browser.$$("ak-form-group")) {
|
||||
// Delightfully, wizards may have slotted elements that *exist* but are not *attached*,
|
||||
// and this can break the damn tests.
|
||||
if (!(await group.isDisplayed())) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
comparator(await group.$("div.pf-c-form__field-group-header-title-text").getText())
|
||||
) {
|
||||
@ -112,8 +117,7 @@ export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
|
||||
const buttons = await context.$$("button");
|
||||
let button: WebdriverIO.Element;
|
||||
for (const b of buttons) {
|
||||
const label = await b.getText();
|
||||
if (label.indexOf(name) !== -1) {
|
||||
if (b.isDisplayed() && (await b.getText()).indexOf(name) !== -1) {
|
||||
button = b;
|
||||
break;
|
||||
}
|
||||
@ -123,16 +127,9 @@ export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
|
||||
await doBlur(button);
|
||||
}
|
||||
|
||||
const tap = <T>(a: T): T => {
|
||||
console.log(a);
|
||||
return a;
|
||||
};
|
||||
|
||||
export async function clickToggleGroup(name: string, value: string | RegExp) {
|
||||
const comparator =
|
||||
typeof name === "string"
|
||||
? (sample) => tap(sample) === tap(value)
|
||||
: (sample) => value.test(sample);
|
||||
typeof name === "string" ? (sample) => sample === value : (sample) => value.test(sample);
|
||||
|
||||
const button = await (async () => {
|
||||
for await (const button of $(`[data-ouid-component-name=${name}]`).$$(
|
||||
|
||||
@ -35,8 +35,8 @@ export const simpleLDAPProviderForm: TestProvider = () => [
|
||||
[clickButton, "Next"],
|
||||
[setTextInput, "name", newObjectName("New LDAP Provider")],
|
||||
// This will never not weird me out.
|
||||
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
|
||||
[setFormGroup, /Flow settings/, "open"],
|
||||
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
|
||||
[setSearchSelect, "invalidationFlow", "default-invalidation-flow"],
|
||||
];
|
||||
|
||||
|
||||
@ -5036,10 +5036,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s3cd84e82e83e35ad">
|
||||
<source>Please enter your code</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Zurück zur Geräteauswahl</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
<target>Authentifizierung erneut versuchen</target>
|
||||
@ -7015,6 +7011,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -5294,10 +5294,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Please enter your code</source>
|
||||
<target>Please enter your code</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Return to device picker</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
<target>Retry authentication</target>
|
||||
@ -7280,6 +7276,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -4962,10 +4962,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s3cd84e82e83e35ad">
|
||||
<source>Please enter your code</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Regresar al selector de dispositivos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
<target>Reintentar la autenticación</target>
|
||||
@ -6932,6 +6928,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -6616,11 +6616,6 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
<source>Please enter your code</source>
|
||||
<target>Veuillez saisir votre code</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Retourner à la sélection d'appareil</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -9131,90 +9126,128 @@ Les liaisons avec les groupes/utilisateurs sont vérifiées par rapport à l'uti
|
||||
</trans-unit>
|
||||
<trans-unit id="sbfee780fa0a2c83e">
|
||||
<source>Device type <x id="0" equiv-text="${device.verboseName}"/> cannot be deleted</source>
|
||||
<target>Le type d'appareil <x id="0" equiv-text="${device.verboseName}"/> ne peut pas être supprimé</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s336936629cdeb3e5">
|
||||
<source>Stage used to verify users' browsers using Google Chrome Device Trust. This stage can be used in authentication/authorization flows.</source>
|
||||
<target>Étape utilisée pour vérifier le navigateur des utilisateurs avec le connecteur de confiance des appareils Google Chrome Enterprise. Cette étape peut être utilisée dans les flux d'authentification et d'autorisation.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s85fe794c71b4ace8">
|
||||
<source>Google Verified Access API</source>
|
||||
<target>API Google Verified Access</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s013620384af7c8b4">
|
||||
<source>Device type <x id="0" equiv-text="${device.verboseName}"/> cannot be edited</source>
|
||||
<target>Le type d'appareil <x id="0" equiv-text="${device.verboseName}"/> ne peut pas être édité</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4347135696fc7cde">
|
||||
<source>Advanced flow settings</source>
|
||||
<target>Paramètres avancés des flux</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf52ff57fd136cc2f">
|
||||
<source>Enable this option to write password changes made in authentik back to Kerberos. Ignored if sync is disabled.</source>
|
||||
<target>Activer cette option pour écrire les changements de mot de passe fait dans authentik dans Kerberos. Ignoré si la synchronisation est désactivée.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s14a16542f956e11d">
|
||||
<source>Realm settings</source>
|
||||
<target>Paramètres du realm</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9c2eae548d3c1c30">
|
||||
<source>Realm</source>
|
||||
<target>Realm</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s6b032212997e2491">
|
||||
<source>Kerberos 5 configuration</source>
|
||||
<target>Configuration Kerberos 5</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbf50181022f47de3">
|
||||
<source>Kerberos 5 configuration. See man krb5.conf(5) for configuration format. If left empty, a default krb5.conf will be used.</source>
|
||||
<target>Configuration Kerbers 5. Cf. man krb5.conf(5) pour le format de configuration. Si laissé vide, un krb5.conf par défaut sera utilisé.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2386539a0bd62fab">
|
||||
<source>Sync connection settings</source>
|
||||
<target>Paramètres de synchronisation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0d1a6f3fe81351f8">
|
||||
<source>Sync principal</source>
|
||||
<target>Principal de synchronisation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa691d6e1974295fa">
|
||||
<source>Principal used to authenticate to the KDC for syncing.</source>
|
||||
<target>Principal utilisé pour s'authentifier au KDC pour synchroniser.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s977b9c629eed3d33">
|
||||
<source>Sync password</source>
|
||||
<target>Mot de passe de synchronisation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s77772860385de948">
|
||||
<source>Password used to authenticate to the KDC for syncing. Optional if Sync keytab or Sync credentials cache is provided.</source>
|
||||
<target>Mot de passe utilisé pour s'authentifier au KDC pour synchroniser. Optional si une keytab de synchronisation ou un credentials cache de synchronisation est fourni.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sc59ec59c3d5e74dc">
|
||||
<source>Sync keytab</source>
|
||||
<target>Keytab de synchronisation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="scd42997958453f05">
|
||||
<source>Keytab used to authenticate to the KDC for syncing. Optional if Sync password or Sync credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.</source>
|
||||
<target>Keytab utilisée pour s'authentifier au KDC pour synchroniser. Optional si un mot de passe de synchronisation ou un credentials cache de synchronisation est fourni. Doit être encodé en base64 ou de la forme TYPE:residual.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s60eaf439ccdca1f2">
|
||||
<source>Sync credentials cache</source>
|
||||
<target>Credentials cache de synchronisation</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s95722900b0c9026f">
|
||||
<source>Credentials cache used to authenticate to the KDC for syncing. Optional if Sync password or Sync keytab is provided. Must be in the form TYPE:residual.</source>
|
||||
<target>Credentials cache utilisé pour s'authentifier au KDC pour synchroniser. Optional si un mot de passe de synchronisation ou une keytab de synchronisation est fourni. Doit être de la forme TYPE:residual.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf9c055db98d7994a">
|
||||
<source>SPNEGO settings</source>
|
||||
<target>Paramètres SPNEGO</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sab580a45dc46937f">
|
||||
<source>SPNEGO server name</source>
|
||||
<target>Nom de serveur SPNEGO</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7a79d6174d17ab2d">
|
||||
<source>Force the use of a specific server name for SPNEGO. Must be in the form HTTP@domain</source>
|
||||
<target>Force l'utilisation d'un nom de serveur spécifique pour SPNEGO. Doit être de la forme HTTP@hostname</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa4ba2b2081472ccd">
|
||||
<source>SPNEGO keytab</source>
|
||||
<target>Keytab SPNEGO</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s64adda975c1106c0">
|
||||
<source>Keytab used for SPNEGO. Optional if SPNEGO credentials cache is provided. Must be base64 encoded or in the form TYPE:residual.</source>
|
||||
<target>Keytab utilisée pour SPNEGO. Optional si un credentials cache SPNEGO est fourni. Doit être encodé en base64 ou de la forme TYPE:residual.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s92247825b92587b5">
|
||||
<source>SPNEGO credentials cache</source>
|
||||
<target>Credentials cache SPNEGO</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd9757c345e4062f8">
|
||||
<source>Credentials cache used for SPNEGO. Optional if SPNEGO keytab is provided. Must be in the form TYPE:residual.</source>
|
||||
<target>Credentials cache utilisé pour SPNEGO. Optional si une keytab SPNEGO est fournie. Doit être de la forme TYPE:residual.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s734ab8fbcae0b69e">
|
||||
<source>Kerberos Attribute mapping</source>
|
||||
<target>Mappage d'attributs Kerberos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2c378e86e025fdb2">
|
||||
<source>Update Kerberos Source</source>
|
||||
<target>Mettre à jour la source Kerberos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
<target>Base de données utilisateurs + mot de passe Kerberos</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -6590,11 +6590,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Please enter your code</source>
|
||||
<target>코드를 입력하세요.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>디바이스 선택기로 돌아가기</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -8849,6 +8844,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -6575,11 +6575,6 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
<source>Please enter your code</source>
|
||||
<target>Voer uw code in</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Terug naar apparaatkeuze</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -8695,6 +8690,15 @@ Bindingen naar groepen/gebruikers worden gecontroleerd tegen de gebruiker van de
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -6620,11 +6620,6 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
<source>Please enter your code</source>
|
||||
<target>Proszę wprowadź swój kod</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Wróć do wyboru urządzeń</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -9114,6 +9109,15 @@ Powiązania z grupami/użytkownikami są sprawdzane względem użytkownika zdarz
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -6578,11 +6578,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Please enter your code</source>
|
||||
<target>Ƥĺēàśē ēńţēŕ ŷōũŕ ćōďē</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Ŕēţũŕń ţō ďēvĩćē ƥĩćķēŕ</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -9154,4 +9149,13 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body></file></xliff>
|
||||
|
||||
@ -6619,11 +6619,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Please enter your code</source>
|
||||
<target>Пожалуйста, введите ваш код</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Вернуться к выбору устройства</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -9177,6 +9172,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -4955,10 +4955,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s3cd84e82e83e35ad">
|
||||
<source>Please enter your code</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>Aygıt seçiciye geri dön</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
<target>Kimlik doğrulamayı yeniden deneyin</target>
|
||||
@ -6925,6 +6921,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -4712,9 +4712,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s3cd84e82e83e35ad">
|
||||
<source>Please enter your code</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
</trans-unit>
|
||||
@ -5863,6 +5860,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@ -6618,11 +6618,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Please enter your code</source>
|
||||
<target>请输入您的代码</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>返回设备选择器</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -9217,6 +9212,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -4999,10 +4999,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s3cd84e82e83e35ad">
|
||||
<source>Please enter your code</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>返回设备选择器</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
<target>重试身份验证</target>
|
||||
@ -6973,6 +6969,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
@ -6566,11 +6566,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>Please enter your code</source>
|
||||
<target>請輸入您的認證碼</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s18b910437b73e8e8">
|
||||
<source>Return to device picker</source>
|
||||
<target>回到選擇裝置頁面</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="se409d01b52c4e12f">
|
||||
<source>Retry authentication</source>
|
||||
@ -8810,6 +8805,15 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s03e4044abe0b556c">
|
||||
<source>User database + Kerberos password</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s98bb2ae796f1ceef">
|
||||
<source>Select another authentication method</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s21d95b4651ad7a1e">
|
||||
<source>Enter a one-time recovery code for this user.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e1d5a7d320c25ef">
|
||||
<source>Enter the code from your authenticator device.</source>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
||||
Reference in New Issue
Block a user