diff --git a/web/src/admin/applications/ApplicationForm.ts b/web/src/admin/applications/ApplicationForm.ts index c8aaefda5..42489f9f3 100644 --- a/web/src/admin/applications/ApplicationForm.ts +++ b/web/src/admin/applications/ApplicationForm.ts @@ -159,7 +159,7 @@ export class ApplicationForm extends ModelForm { value=${this.instance?.group} label=${msg("Group")} help=${msg( - "Optionally enter a group name. Applications with identical groups are shown grouped together." + "Optionally enter a group name. Applications with identical groups are shown grouped together.", )} > { name="backchannelProviders" label=${msg("Backchannel Providers")} help=${msg( - "Select backchannel providers which augment the functionality of the main provider." + "Select backchannel providers which augment the functionality of the main provider.", )} .providers=${this.backchannelProviders} .confirm=${this.handleConfirmBackchannelProviders} @@ -199,7 +199,7 @@ export class ApplicationForm extends ModelForm { label=${msg("Launch URL")} value=${ifDefined(this.instance?.metaLaunchUrl)} help=${msg( - "If left empty, authentik will try to extract the launch URL based on the selected provider." + "If left empty, authentik will try to extract the launch URL based on the selected provider.", )} > { ?checked=${first(this.instance?.openInNewTab, false)} label=${msg("Open in new tab")} help=${msg( - "If checked, the launch URL will open in a new browser tab or window from the user's application library." + "If checked, the launch URL will open in a new browser tab or window from the user's application library.", )} > diff --git a/web/src/admin/applications/components/ak-backchannel-input.ts b/web/src/admin/applications/components/ak-backchannel-input.ts index 99cbfb30f..2130ba10b 100644 --- a/web/src/admin/applications/components/ak-backchannel-input.ts +++ b/web/src/admin/applications/components/ak-backchannel-input.ts @@ -56,12 +56,12 @@ export class AkBackchannelProvidersInput extends AKElement { html`${provider.name}`; return html` - +
- ${map(providers, renderOneChip)} + ${map(this.providers, renderOneChip)}
- ${help ? html`

${help}

` : nothing} + ${this.help ? html`

${this.help}

` : nothing}
`; } diff --git a/web/src/admin/applications/wizard/proxy/ak-application-wizard-authentication-by-proxy.ts b/web/src/admin/applications/wizard/proxy/ak-application-wizard-authentication-by-proxy.ts new file mode 100644 index 000000000..ea7df221e --- /dev/null +++ b/web/src/admin/applications/wizard/proxy/ak-application-wizard-authentication-by-proxy.ts @@ -0,0 +1,371 @@ +import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; +import { first } from "@goauthentik/common/utils"; +import "@goauthentik/elements/forms/HorizontalFormElement"; + +import { msg } from "@lit/localize"; +import { customElement, state } from "@lit/reactive-element/decorators.js"; +import { TemplateResult, html } from "lit"; +import { ifDefined } from "lit/directives/if-defined.js"; + +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 { + FlowsInstancesListDesignationEnum, + PaginatedOAuthSourceList, + PaginatedScopeMappingList, + PropertymappingsApi, + ProxyMode, + ProxyProvider, + SourcesApi, +} from "@goauthentik/api"; + +import ApplicationWizardPageBase from "../ApplicationWizardPageBase"; + +@customElement("ak-application-wizard-authentication-by-proxy") +export class AkTypeProxyApplicationWizardPage extends ApplicationWizardPageBase { + constructor() { + super(); + new PropertymappingsApi(DEFAULT_CONFIG) + .propertymappingsScopeList({ ordering: "scope_name" }) + .then((propertyMappings: PaginatedScopeMappingList) => { + this.propertyMappings = propertyMappings; + }); + + new SourcesApi(DEFAULT_CONFIG) + .sourcesOauthList({ + ordering: "name", + hasJwks: true, + }) + .then((oauthSources: PaginatedOAuthSourceList) => { + this.oauthSources = oauthSources; + }); + } + + handleChange(ev: InputEvent) { + if (!ev.target) { + console.warn(`Received event with no target: ${ev}`); + return; + } + const target = ev.target as HTMLInputElement; + const value = target.type === "checkbox" ? target.checked : target.value; + this.dispatchWizardUpdate({ + provider: { + ...this.wizard.provider, + [target.name]: value, + }, + }); + } + + propertyMappings?: PaginatedScopeMappingList; + oauthSources?: PaginatedOAuthSourceList; + + @state() + showHttpBasic = true; + + @state() + mode: ProxyMode = ProxyMode.Proxy; + + get instance(): ProxyProvider | undefined { + return this.wizard.provider as ProxyProvider; + } + + renderHttpBasic(): TemplateResult { + return html` + + + + `; + } + + renderModeSelector(): TemplateResult { + const setMode = (ev: CustomEvent<{ value: ProxyMode }>) => { + this.mode = ev.detail.value; + }; + + // prettier-ignore + return html` + + + + + + `; + } + + renderProxyModeProxy() { + return html`

+ ${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.", + )} +

+ + + + `; + } + + renderProxyModeForwardSingle() { + return html`

+ ${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 manged outpost, this is done for you).", + )} +

+ `; + } + + renderProxyModeForwardDomain() { + return html`

+ ${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.", + )} +

+
+ ${msg("An example setup can look like this:")} +
    +
  • ${msg("authentik running on auth.example.com")}
  • +
  • ${msg("app1 running on app1.example.com")}
  • +
+ ${msg( + "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com.", + )} +
+ + `; + } + + renderSettings() { + switch (this.mode) { + case ProxyMode.Proxy: + return this.renderProxyModeProxy(); + case ProxyMode.ForwardSingle: + return this.renderProxyModeForwardSingle(); + case ProxyMode.ForwardDomain: + return this.renderProxyModeForwardDomain(); + case ProxyMode.UnknownDefaultOpenApi: + return html`

${msg("Unknown proxy mode")}

`; + } + } + + render() { + return html`
+ + + +

+ ${msg("Flow used when a user access this provider and is not authenticated.")} +

+
+ + +

+ ${msg("Flow used when authorizing this provider.")} +

+
+ +
+
${this.renderModeSelector()}
+ +
+ + + + ${msg("Advanced protocol settings")} +
+ + + + + +

+ ${msg("Additional scope mappings, which are passed to the proxy.")} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+ + ${msg( + "Regular expressions for which authentication is not required. Each new line is interpreted as a new expression.", + )} +

+

+ ${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.", + )} +

`} + > +
+
+
+ + ${msg("Authentication settings")} +
+ + + { + 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.", + )} + > + + ${this.showHttpBasic ? this.renderHttpBasic() : html``} + + + +

+ ${msg( + "JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.", + )} +

+

+ ${msg("Hold control/command to select multiple items.")} +

+
+
+
+
`; + } +} + +export default AkTypeProxyApplicationWizardPage; diff --git a/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts b/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts index e1f5e71ae..203aded62 100644 --- a/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts +++ b/web/src/admin/applications/wizard/stories/ak-application-wizard.stories.ts @@ -7,6 +7,7 @@ import AkApplicationWizardApplicationDetails from "../ak-application-wizard-appl import "../ak-application-wizard-authentication-method-choice"; import "../ak-application-wizard-context"; import "../ldap/ak-application-wizard-authentication-by-ldap"; +import "../proxy/ak-application-wizard-authentication-by-proxy"; import "../oauth/ak-application-wizard-authentication-by-oauth"; import "./ak-application-context-display-for-test"; import { @@ -141,3 +142,13 @@ export const PageThreeOauth2 = () => { `, ); }; + +export const PageThreeProxy = () => { + return container( + html` + +
+ +
`, + ); +}; diff --git a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts index 0aac39deb..a8d2f64f6 100644 --- a/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts +++ b/web/src/admin/providers/oauth2/OAuth2ProviderForm.ts @@ -13,7 +13,7 @@ import "@goauthentik/elements/forms/SearchSelect"; import "@goauthentik/elements/utils/TimeDeltaHelp"; import { msg } from "@lit/localize"; -import { TemplateResult, html, nothing } from "lit"; +import { TemplateResult, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { ifDefined } from "lit/directives/if-defined.js";