Compare commits

...

20 Commits

Author SHA1 Message Date
76b9add6b4 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main: (142 commits)
  core: bump goauthentik.io/api/v3 from 3.2024102.2 to 3.2024104.1 (#12149)
  core: bump debugpy from 1.8.8 to 1.8.9 (#12150)
  core: bump webauthn from 2.2.0 to 2.3.0 (#12151)
  core: bump pydantic from 2.10.0 to 2.10.1 (#12152)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#12156)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#12157)
  core: bump sentry-sdk from 2.18.0 to 2.19.0 (#12153)
  web: bump API Client version (#12147)
  root: Backport version change (#12146)
  website/docs: update info about footer links to match new UI (#12120)
  website/docs: prepare release notes (#12142)
  providers/oauth2: fix migration (#12138)
  providers/oauth2: fix migration dependencies (#12123)
  web: bump API Client version (#12129)
  providers/oauth2: fix redirect uri input (#12122)
  providers/proxy: fix redirect_uri (#12121)
  website/docs: prepare release notes (#12119)
  web: bump API Client version (#12118)
  security: fix CVE 2024 52289 (#12113)
  security: fix CVE 2024 52307 (#12115)
  ...
2024-11-22 10:01:43 -08:00
d76e3c8023 Unneeded console.log 2024-11-01 12:46:24 -07:00
fb40ee72a5 Fix merge bug; modify package to always build before testing to catch bugs of this kind. 2024-11-01 10:46:43 -07:00
24f52252ba Merge bug! Lesson of the day: always run npm run build && npm run test:e2e in that order! 2024-11-01 10:27:40 -07:00
26ceb3d6c9 How did that get through? 2024-11-01 10:20:44 -07:00
f756be2ece Merge branch 'main' into web/update-provider-forms-for-invalidation
* main:
  website: bump elliptic from 6.5.7 to 6.6.0 in /website (#11869)
  core: bump selenium from 4.25.0 to 4.26.0 (#11875)
  core: bump goauthentik.io/api/v3 from 3.2024083.14 to 3.2024100.1 (#11876)
  website/docs: add info about invalidation flow, default flows in general (#11800)
  website: fix docs redirect (#11873)
  website: remove RC disclaimer for version 2024.10 (#11871)
  website: update supported versions (#11841)
  web: bump API Client version (#11870)
  root: backport version bump 2024.10.0 (#11868)
  website/docs: 2024.8.4 release notes (#11862)
  web/admin: provide default invalidation flows for LDAP and Radius (#11861)
  core, web: update translations (#11858)
2024-10-31 09:33:14 -07:00
4bfd06e034 Labeling error. 2024-10-30 16:52:22 -07:00
107dff39af Updating these to correspond to changes in MAIN. 2024-10-30 10:36:54 -07:00
401850c5e2 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main:
  web/admin: fix code-based MFA toggle not working in wizard (#11854)
  sources/kerberos: add kiprop to ignored system principals (#11852)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#11846)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#11845)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#11847)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11848)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#11849)
  translate: Updates for file web/xliff/en.xlf in it (#11850)
  website: 2024.10 Release Notes (#11839)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#11814)
  core, web: update translations (#11821)
  core: bump goauthentik.io/api/v3 from 3.2024083.13 to 3.2024083.14 (#11830)
  core: bump service-identity from 24.1.0 to 24.2.0 (#11831)
  core: bump twilio from 9.3.5 to 9.3.6 (#11832)
  core: bump pytest-randomly from 3.15.0 to 3.16.0 (#11833)
  website/docs: Update social-logins github (#11822)
  website/docs: remove � (#11823)
  lifecycle: fix kdc5-config missing (#11826)
  website/docs: update preview status of different features (#11817)
2024-10-29 15:56:05 -07:00
11bc9b8041 Not sure how *that* got lost, but... 2024-10-29 15:51:19 -07:00
807e2a9fb0 web/admin: Unify the forms for providers between the ./admin/providers and ./admin/applications/wizard
## What

- For LDAP, OAuth2, Radius, SAML, SCIM, and Proxy providers, extract the literal form rendering
  component of each provider into a function.  After all, that's what they are: they take input (the
  render state) and produce output (HTML with event handlers).
- Rip out all of the forms in the wizard and replace them with ☝️
- Write E2E tests that exercise *all* of the components in *all* of the forms mentioned. See test
  results.  These tests come in two flavors, "simple" (minimum amount needed to make the provider
  "pass" the backend's parsers) and "complete" (touches every legal field in the form according to
  the authentik `./schema.yml` file).  As a result, every field is validated against the schema
  (although the schema is currently ported into the test by hand.
- Fixed some serious bugginess in the way the wizard `commit` phase handles errors.

## Details

### Providers

In some cases, I broke up the forms into smaller units:

- Proxy, especially, with standalone units now for `renderHttpBasic`, `renderModeSelector`,
`renderSettings`, and the differing modes)
- SAML now has a `renderHasSigningKp` object, which makes that part of the code much more readable.

I also extracted a few of static `options` collections into static const objects, so that the form
object itself would be a bit more readable.

### Wizard

Just ripped out all of the Provider forms.  All of them.  They weren't going to be needed in our
glorious new future.

Using the information provided by the `providerTypes` object, it was easy to extract all of the
information that had once been in `ak-application-wizard-authentication-method-choice.choices`. The
only thing left now is the renderers, one for each of the forms ripped out. Everything else is just
gone.

As a result, though, that's no longer a static list. It has to be derived from information sent via
the API.  So now it's in a context that's built when the wizard is initialized, and accessed by the
`createTypes` pass as well as the specific provider.

The error handling in the `commit` pass was just broken.  I have improved it quite a bit, and now it
actually displays helpful messages when things go wrong.

### Tests

Wrote a simple test runner that iterates through a collection of fields, setting their values via
field-type instructions contained in each line. For example, the "simple" OAuth2 Provider test looks
like this:

```
export const simpleOAuth2ProviderForm: TestProvider = () => [
    [setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
    [clickButton, "Next"],
    [setTextInput, "name", newObjectName("New Oauth2 Provider")],
    [setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
];
```

Each control checks for the existence of the object, and in most cases its current `display`.
(SearchSelect only checks existence, due to the oddness of the portaled popup.)  Where a field can't
reasonably be modified and still pass, we at least verify that the name provided in `schema.yml`
corresponds to an existing, available control on the form or wizard panel.

Combined with a routine for logging in and navigating to the Provider page, and another one to
validate that a new and uniqute "Successfully Created Provider" notification appeared, this makes
testing each provider a simple message of filling out the table of fields you want populated.

Equally simple: these *exact same tests* can be incorporated into a wrapper for logging in,
navigating to the Application page, and filling out an Application, and then a new and unique
Provider for that Application, by Provider Type.

As a special case, the Wizard variant checks the `TestSequence` object returned by the
`TestProvider` function and removes the `name` field, since the Wizard pre-populates that
automatically.

As a result of this, the contents of `./web/src` has lost 1,504 lines of code. And results like
these, where the behavior has been cross-checked three ways (the forms, the tests (and so the
back-end), *and the schema* all agree on field names and behaviors, gives me much more confidence
that the refactor works as expected:

```
[chrome 130.0.6723.70 mac #0-1] Running: chrome (v130.0.6723.70) on mac
[chrome 130.0.6723.70 mac #0-1] Session ID: 039c70690eebc83ffbc2eef97043c774
[chrome 130.0.6723.70 mac #0-1]
[chrome 130.0.6723.70 mac #0-1] » /tests/specs/providers.ts
[chrome 130.0.6723.70 mac #0-1] Configuring Providers
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple LDAP provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple OAuth2 provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Radius provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple SAML provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple SCIM provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Proxy provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Forward Auth (single application) provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Simple Forward Auth (domain level) provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete OAuth2 provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete LDAP provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Radius provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete SAML provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete SCIM provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Proxy provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Forward Auth (single application) provider
[chrome 130.0.6723.70 mac #0-1]    ✓ Should successfully configure a Complete Forward Auth (domain level) provider
[chrome 130.0.6723.70 mac #0-1]
[chrome 130.0.6723.70 mac #0-1] 16 passing (1m 48.5s)
------------------------------------------------------------------
[chrome 130.0.6723.70 mac #0-2] Running: chrome (v130.0.6723.70) on mac
[chrome 130.0.6723.70 mac #0-2] Session ID: 5a3ae12c851eff8fffd2686096759146
[chrome 130.0.6723.70 mac #0-2]
[chrome 130.0.6723.70 mac #0-2] » /tests/specs/new-application-by-wizard.ts
[chrome 130.0.6723.70 mac #0-2] Configuring Applications Via the Wizard
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple LDAP provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple OAuth2 provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Radius provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple SAML provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple SCIM provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Proxy provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Forward Auth (single) provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Simple Forward Auth (domain) provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete OAuth2 provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete LDAP provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Radius provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete SAML provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete SCIM provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Proxy provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Forward Auth (single) provider
[chrome 130.0.6723.70 mac #0-2]    ✓ Should successfully configure an application with a Complete Forward Auth (domain) provider
[chrome 130.0.6723.70 mac #0-2]
[chrome 130.0.6723.70 mac #0-2] 16 passing (2m 3s)
```

🎉
2024-10-29 15:50:51 -07:00
5bd7cedaba 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)
  ...
2024-10-28 09:37:16 -07:00
c0814ad279 Almost there! 2024-10-25 10:27:06 -07:00
99af95b10c Committed harmony on SAML. Streamlined the tests even further. 2024-10-24 09:35:31 -07:00
a36cc820bd Radius form has been isolated. 2024-10-23 15:24:47 -07:00
6ff260df01 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main:
  web/admin: Add InvalidationFlow to Radius Provider dialogues (#11786)
  core, web: update translations (#11782)
  providers/oauth2: fix amr claim not set due to login event not associated (#11780)
2024-10-23 14:45:21 -07:00
e497dbc314 Merge branch 'main' into web/update-provider-forms-for-invalidation
* main: (44 commits)
  web/admin: add strict dompurify config for diagram (#11783)
  core: bump cryptography from 43.0.1 to 43.0.3 (#11750)
  web: bump API Client version (#11781)
  sources: add Kerberos (#10815)
  root: rework CSRF middleware to set secure flag (#11753)
  web/admin: improve invalidation flow default & field grouping (#11769)
  providers/scim: add comparison with existing group on update and delta update users (#11414)
  website: bump mermaid from 10.6.0 to 10.9.3 in /website (#11766)
  web/flows: use dompurify for footer links (#11773)
  core, web: update translations (#11775)
  core: bump goauthentik.io/api/v3 from 3.2024083.10 to 3.2024083.11 (#11776)
  website: bump @types/react from 18.3.11 to 18.3.12 in /website (#11777)
  website: bump http-proxy-middleware from 2.0.6 to 2.0.7 in /website (#11771)
  web: bump API Client version (#11770)
  stages: authenticator_endpoint_gdtc (#10477)
  core: add prompt_data to auth flow (#11702)
  tests/e2e: fix dex tests failing (#11761)
  web/rac: disable DPI scaling (#11757)
  web/admin: update flow background (#11758)
  website/docs: fix some broken links (#11742)
  ...
2024-10-23 14:00:31 -07:00
f9f849574b We have working tests!!!!!! 2024-10-23 10:50:27 -07:00
4439b298bd Still trying to find components by internal text. Still not working. 2024-10-22 14:21:16 -07:00
4af6ecf629 web: Isolate the OAuth2 Provider Form into a reusable rendering function
- Pull the OAuth2 Provider Form `render()` method out into a standalone function.
  - Why: So it can be shared by both the Wizard and the Provider function. The renderer is (or at
    least, can be) a pure function: you give it input and it produces HTML, *and then it stops*.
- Provide a test harness that can test the OAuth2 provider form.
2024-10-22 07:13:04 -07:00
43 changed files with 3215 additions and 4572 deletions

1620
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -135,6 +135,7 @@
"storybook:build": "wireit",
"storybook:build-import-map": "wireit",
"test": "wireit",
"test:e2e": "wireit",
"test:e2e:watch": "wireit",
"test:watch": "wireit",
"tsc": "wireit",
@ -321,11 +322,24 @@
},
"test": {
"command": "wdio ./wdio.conf.ts --logLevel=warn",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "tsconfig.test.json"
}
},
"test:e2e": {
"command": "wdio run ./tests/wdio.conf.ts",
"dependencies": [
"build"
],
"env": {
"CI": "true",
"TS_NODE_PROJECT": "./tests/tsconfig.test.json"
}
},
"test:e2e:watch": {
"command": "wdio run ./tests/wdio.conf.ts",
"dependencies": [

View File

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

View File

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

View File

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

View File

@ -1,176 +1,28 @@
import "@goauthentik/admin/common/ak-license-notice";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import type { ProviderModelEnum as ProviderModelEnumType, TypeCreate } from "@goauthentik/api";
import { ProviderModelEnum, ProxyMode } from "@goauthentik/api";
import type {
LDAPProviderRequest,
ModelRequest,
OAuth2ProviderRequest,
ProxyProviderRequest,
RACProviderRequest,
RadiusProviderRequest,
SAMLProviderRequest,
SCIMProviderRequest,
} from "@goauthentik/api";
import { OneOfProvider } from "../types";
import type { TypeCreate } from "@goauthentik/api";
type ProviderRenderer = () => TemplateResult;
type ModelConverter = (provider: OneOfProvider) => ModelRequest;
type ProviderNoteProvider = () => TemplateResult | undefined;
type ProviderNote = ProviderNoteProvider | undefined;
export type LocalTypeCreate = TypeCreate & {
formName: string;
modelName: ProviderModelEnumType;
converter: ModelConverter;
note?: ProviderNote;
renderer: ProviderRenderer;
};
export const providerModelsList: LocalTypeCreate[] = [
{
formName: "oauth2provider",
name: msg("OAuth2/OIDC (Open Authorization/OpenID Connect)"),
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 (Lightweight Directory Access Protocol)"),
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 (Security Assertion Markup Language)"),
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 (Remote Authentication Dial-In User Service)"),
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 (System for Cross-domain Identity Management)"),
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: Record<string, () => TemplateResult> = {
oauth2provider: () =>
html`<ak-application-wizard-authentication-by-oauth></ak-application-wizard-authentication-by-oauth>`,
ldapprovider: () =>
html`<ak-application-wizard-authentication-by-ldap></ak-application-wizard-authentication-by-ldap>`,
proxyprovider: () =>
html`<ak-application-wizard-authentication-for-reverse-proxy></ak-application-wizard-authentication-for-reverse-proxy>`,
racprovider: () =>
html`<ak-application-wizard-authentication-for-rac></ak-application-wizard-authentication-for-rac>`,
samlprovider: () =>
html`<ak-application-wizard-authentication-by-saml-configuration></ak-application-wizard-authentication-by-saml-configuration>`,
radiusprovider: () =>
html`<ak-application-wizard-authentication-by-radius></ak-application-wizard-authentication-by-radius>`,
scimprovider: () =>
html`<ak-application-wizard-authentication-by-scim></ak-application-wizard-authentication-by-scim>`,
};

View File

@ -7,41 +7,37 @@ 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}
@select=${(ev: CustomEvent<LocalTypeCreate>) => {
this.dispatchWizardUpdate({
update: {
...this.wizard,
providerModel: ev.detail.formName,
providerModel: ev.detail.modelName,
errors: {},
},
status: this.valid ? "valid" : "invalid",

View File

@ -22,6 +22,9 @@ import {
type ApplicationRequest,
CoreApi,
type ModelRequest,
ProviderModelEnum,
ProxyMode,
type ProxyProviderRequest,
type TransactionApplicationRequest,
type TransactionApplicationResponse,
ValidationError,
@ -29,7 +32,6 @@ import {
} 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 {
@ -39,14 +41,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: "",
@ -70,6 +77,7 @@ const successState: State = {
icon: ["fa-check-circle", "pf-m-success"],
};
type StrictProviderModelEnum = Exclude<ProviderModelEnum, "11184809">;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isValidationError = (v: any): v is ValidationError => instanceOfValidationError(v);
@ -102,19 +110,28 @@ 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,
) as StrictProviderModelEnum;
const provider = this.wizard.provider as ModelRequest;
provider.providerModel = providerModel;
// Special case for the Proxy provider.
if (this.wizard.providerModel === "proxyprovider") {
(provider as ProxyProviderRequest).mode = this.wizard.proxyMode;
if ((provider as ProxyProviderRequest).mode !== ProxyMode.ForwardDomain) {
(provider as ProxyProviderRequest).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);
@ -125,6 +142,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
data: TransactionApplicationRequest,
): Promise<TransactionApplicationResponse | void> {
this.errors = undefined;
this.commitState = idleState;
new CoreApi(DEFAULT_CONFIG)
.coreTransactionalApplicationsUpdate({
transactionApplicationRequest: data,
@ -138,6 +156,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.catch(async (resolution: any) => {
const errors = await parseAPIError(resolution);
console.log(errors);
// THIS is a really gross special case; if the user is duplicating the name of an
// existing provider, the error appears on the `app` (!) error object. We have to
@ -164,11 +183,7 @@ export class ApplicationWizardCommitApplication extends BasePanel {
});
}
renderErrors(errors?: ValidationError) {
if (!errors) {
return nothing;
}
renderErrors(errors: ValidationError) {
const navTo = (step: number) => () =>
this.dispatchCustomEvent("ak-wizard-nav", {
command: "goto",
@ -219,7 +234,9 @@ export class ApplicationWizardCommitApplication extends BasePanel {
>
${this.commitState.label}
</h1>
${this.renderErrors(this.errors)}
${this.commitState === errorState
? this.renderErrors(this.errors ?? {})
: nothing}
</div>
</div>
</div>

View File

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

View File

@ -1,33 +1,14 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import { renderForm } from "@goauthentik/admin/providers/ldap/LDAPProviderFormForm.js";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement } from "@lit/reactive-element/decorators/custom-element.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { html } from "lit";
import { FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import type { LDAPProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
import {
bindModeOptions,
cryptoCertificateHelp,
gidStartNumberHelp,
mfaSupportHelp,
searchModeOptions,
tlsServerNameHelp,
uidStartNumberHelp,
} from "./LDAPOptionsAndHelp";
@customElement("ak-application-wizard-authentication-by-ldap")
export class ApplicationWizardApplicationDetails extends WithBrandConfig(BaseProviderPanel) {
@ -37,129 +18,7 @@ export class ApplicationWizardApplicationDetails extends WithBrandConfig(BasePro
return html` <ak-wizard-title>${msg("Configure LDAP Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Bind flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${this.brand.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Unbind flow")}
name="invalidationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.brandFlow=${this.brand.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
</ak-form-element-horizontal>
<ak-radio-input
label=${msg("Bind mode")}
name="bindMode"
.options=${bindModeOptions}
.value=${provider?.bindMode}
help=${msg("Configure how the outpost authenticates requests.")}
>
</ak-radio-input>
<ak-radio-input
label=${msg("Search mode")}
name="searchMode"
.options=${searchModeOptions}
.value=${provider?.searchMode}
help=${msg(
"Configure how the outpost queries the core authentik server's users.",
)}
>
</ak-radio-input>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="baseDn"
label=${msg("Base DN")}
required
value="${first(provider?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
.errorMessages=${errors?.baseDn ?? []}
help=${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate"
>
</ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
</ak-form-element-horizontal>
<ak-text-input
label=${msg("TLS Server name")}
name="tlsServerName"
value="${first(provider?.tlsServerName, "")}"
.errorMessages=${errors?.tlsServerName ?? []}
help=${tlsServerNameHelp}
></ak-text-input>
<ak-number-input
label=${msg("UID start number")}
required
name="uidStartNumber"
value="${first(provider?.uidStartNumber, 2000)}"
.errorMessages=${errors?.uidStartNumber ?? []}
help=${uidStartNumberHelp}
></ak-number-input>
<ak-number-input
label=${msg("GID start number")}
required
name="gidStartNumber"
value="${first(provider?.gidStartNumber, 4000)}"
.errorMessages=${errors?.gidStartNumber ?? []}
help=${gidStartNumberHelp}
></ak-number-input>
</div>
</ak-form-group>
${renderForm(provider, errors, this.brand)}
</form>`;
}
}

View File

@ -1,47 +1,11 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2PropertyMappings.js";
import {
clientTypeOptions,
issuerModeOptions,
redirectUriHelp,
subjectModeOptions,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import {
makeSourceSelector,
oauth2SourcesProvider,
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { html } from "lit";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
MatchingModeEnum,
RedirectURI,
SourcesApi,
} from "@goauthentik/api";
import { SourcesApi } from "@goauthentik/api";
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@ -69,258 +33,17 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
render() {
const provider = this.wizard.provider as OAuth2Provider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-wizard-title>${msg("Configure OAuth2/OpenId Provider")}</ak-wizard-title>
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return html` <ak-wizard-title>${msg("Configure OAuth2 Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
.errorMessages=${errors?.authorizationFlow ?? []}
?required=${true}
>
<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>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
.value=${provider?.clientType}
required
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
}}
.options=${clientTypeOptions}
>
</ak-radio-input>
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value=${provider?.clientId ?? randomString(40, ascii_letters + digits)}
.errorMessages=${errors?.clientId ?? []}
required
>
</ak-text-input>
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value=${provider?.clientSecret ??
randomString(128, ascii_letters + digits)}
.errorMessages=${errors?.clientSecret ?? []}
?hidden=${!this.showClientSecret}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Redirect URIs/Origins")}
required
name="redirectUris"
>
<ak-array-input
.items=${[]}
.newItem=${() => ({
matchingMode: MatchingModeEnum.Strict,
url: "",
})}
.row=${(f?: RedirectURI) =>
akOAuthRedirectURIInput({
".redirectURI": f,
"style": "width: 100%",
"name": "oauth2-redirect-uri",
} as unknown as IRedirectURIInput)}
>
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Signing Key")}
name="signingKey"
.errorMessages=${errors?.signingKey ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKey ?? nothing)}
name="certificate"
singleton
>
</ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("Key used to sign the tokens.")}
</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
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<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>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
required
value="${first(provider?.accessCodeValidity, "minutes=1")}"
.errorMessages=${errors?.accessCodeValidity ?? []}
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${first(provider?.accessTokenValidity, "minutes=5")}"
required
.errorMessages=${errors?.accessTokenValidity ?? []}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long access tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${first(provider?.refreshTokenValidity, "days=30")}"
.errorMessages=${errors?.refreshTokenValidity ?? []}
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long refresh tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Scopes")}
name="propertyMappings"
.errorMessages=${errors?.propertyMappings ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2PropertyMappingsProvider}
.selector=${makeOAuth2PropertyMappingsSelector(
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(
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
)}
</p>
</ak-form-element-horizontal>
<ak-radio-input
name="subMode"
label=${msg("Subject mode")}
required
.options=${subjectModeOptions}
.value=${provider?.subMode}
help=${msg(
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
)}
>
</ak-radio-input>
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${first(provider?.includeClaimsInIdToken, true)}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}
></ak-switch-input>
<ak-radio-input
name="issuerMode"
label=${msg("Issuer mode")}
required
.options=${issuerModeOptions}
.value=${provider?.issuerMode}
help=${msg(
"Configure how the issuer field of the ID Token should be filled.",
)}
>
</ak-radio-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
.errorMessages=${errors?.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>
${renderForm(
provider ?? {},
errors,
this.showClientSecret,
showClientSecretCallback,
)}
</form>`;
}
}

View File

@ -1,267 +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>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Authentication settings")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="interceptHeaderAuth"
?checked=${first(this.instance?.interceptHeaderAuth, true)}
label=${msg("Intercept header authentication")}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
></ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
?checked=${first(this.instance?.basicAuthEnabled, false)}
@change=${(ev: Event) => {
const el = ev.target as HTMLInputElement;
this.showHttpBasic = el.checked;
}}
label=${msg("Send HTTP-Basic Authentication")}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
></ak-switch-input>
${this.showHttpBasic ? this.renderHttpBasic() : html``}
<ak-form-element-horizontal
label=${msg("Trusted OIDC Sources")}
name="jwksSources"
.errorMessages=${errors?.jwksSources ?? []}
>
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(this.instance?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
</form>`;
}
}
export default AkTypeProxyApplicationWizardPage;

View File

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

View File

@ -1,55 +1,50 @@
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import {
ProxyModeValue,
type SetMode,
type SetShowHttpBasic,
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 { ProxyMode } 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 ?? ProxyMode.Proxy,
onSetMode,
showHttpBasic: this.showHttpBasic,
onSetShowHttpBasic,
})}
</form>`;
}
}

View File

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

View File

@ -1,7 +1,7 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { renderForm } from "@goauthentik/admin/providers/radius/RadiusProviderFormForm.js";
import "@goauthentik/components/ak-text-input";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/forms/FormGroup";
@ -10,91 +10,21 @@ import "@goauthentik/elements/forms/HorizontalFormElement";
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 { FlowsInstancesListDesignationEnum, RadiusProvider } from "@goauthentik/api";
import { RadiusProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@customElement("ak-application-wizard-authentication-by-radius")
export class ApplicationWizardAuthenticationByRadius extends WithBrandConfig(BaseProviderPanel) {
render() {
const provider = this.wizard.provider as RadiusProvider | undefined;
const errors = this.wizard.errors.provider;
return html`<ak-wizard-title>${msg("Configure Radius Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${this.brand.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${first(
provider?.sharedSecret,
randomString(128, ascii_letters + digits),
)}
required
></ak-text-input>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
.errorMessages=${errors?.clientNetworks ?? []}
required
help=${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
will be dropped.`)}
></ak-text-input>
</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("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-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
>
${renderForm(
this.wizard.provider as RadiusProvider | undefined,
this.wizard.errors.provider,
this.brand,
)}
</form>`;
}
}

View File

@ -1,356 +1,35 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-multi-select";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { renderForm } from "@goauthentik/admin/providers/saml/SAMLProviderFormForm.js";
import { msg } from "@lit/localize";
import { customElement, state } from "@lit/reactive-element/decorators.js";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { html } from "lit";
import {
FlowsInstancesListDesignationEnum,
PaginatedSAMLPropertyMappingList,
PropertymappingsApi,
SAMLProvider,
} from "@goauthentik/api";
import { SAMLProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
spBindingOptions,
} from "./SamlProviderOptions";
import "./saml-property-mappings-search";
@customElement("ak-application-wizard-authentication-by-saml-configuration")
export class ApplicationWizardProviderSamlConfiguration extends BaseProviderPanel {
@state()
propertyMappings?: PaginatedSAMLPropertyMappingList;
@state()
hasSigningKp = false;
constructor() {
super();
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsProviderSamlList({
ordering: "saml_name",
})
.then((propertyMappings: PaginatedSAMLPropertyMappingList) => {
this.propertyMappings = propertyMappings;
});
}
propertyMappingConfiguration(provider?: SAMLProvider) {
const propertyMappings = this.propertyMappings?.results ?? [];
const configuredMappings = (providerMappings: string[]) =>
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
const managedMappings = () =>
propertyMappings
.filter((pm) => (pm?.managed ?? "").startsWith("goauthentik.io/providers/saml"))
.map((pm) => pm.pk);
const pmValues = provider?.propertyMappings
? configuredMappings(provider?.propertyMappings ?? [])
: managedMappings();
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
return { pmValues, propertyPairs };
}
render() {
const provider = this.wizard.provider as SAMLProvider | undefined;
const errors = this.wizard.errors.provider;
const { pmValues, propertyPairs } = this.propertyMappingConfiguration(provider);
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
};
return html` <ak-wizard-title>${msg("Configure SAML Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
required
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Authorization flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.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>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="acsUrl"
value=${ifDefined(provider?.acsUrl)}
required
label=${msg("ACS URL")}
.errorMessages=${errors?.acsUrl ?? []}
></ak-text-input>
<ak-text-input
name="issuer"
value=${provider?.issuer || "authentik"}
required
label=${msg("Issuer")}
help=${msg("Also known as EntityID.")}
.errorMessages=${errors?.issuer ?? []}
></ak-text-input>
<ak-radio-input
name="spBinding"
label=${msg("Service Provider Binding")}
required
.options=${spBindingOptions}
.value=${provider?.spBinding}
help=${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
>
</ak-radio-input>
<ak-text-input
name="audience"
value=${ifDefined(provider?.audience)}
label=${msg("Audience")}
.errorMessages=${errors?.audience ?? []}
></ak-text-input>
</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
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<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>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Signing Certificate")}
name="signingKp"
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKp ?? undefined)}
@input=${(ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
}}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"Certificate used to sign outgoing Responses going to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
${this.hasSigningKp
? html` <ak-form-element-horizontal name="signAssertion">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.signAssertion, 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("Sign assertions")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="signResponse">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(provider?.signResponse, false)}
/>
<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("Sign responses")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>`
: nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.verificationKp ?? undefined)}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Encryption Certificate")}
name="encryptionKp"
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.encryptionKp ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, encrypted assertions will be decrypted using this keypair.",
)}
</p>
</ak-form-element-horizontal>
<ak-multi-select
label=${msg("Property Mappings")}
name="propertyMappings"
.options=${propertyPairs}
.values=${pmValues}
.richhelp=${html` <p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>`}
></ak-multi-select>
<ak-form-element-horizontal
label=${msg("NameID Property Mapping")}
name="nameIdMapping"
>
<ak-saml-property-mapping-search
name="nameIdMapping"
propertymapping=${ifDefined(provider?.nameIdMapping ?? undefined)}
></ak-saml-property-mapping-search>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="assertionValidNotBefore"
value=${provider?.assertionValidNotBefore || "minutes=-5"}
required
label=${msg("Assertion valid not before")}
help=${msg(
"Configure the maximum allowed time drift for an assertion.",
)}
.errorMessages=${errors?.assertionValidNotBefore ?? []}
></ak-text-input>
<ak-text-input
name="assertionValidNotOnOrAfter"
value=${provider?.assertionValidNotOnOrAfter || "minutes=5"}
required
label=${msg("Assertion valid not on or after")}
help=${msg(
"Assertion not valid on or after current time + this value.",
)}
.errorMessages=${errors?.assertionValidNotOnOrAfter ?? []}
></ak-text-input>
<ak-text-input
name="sessionValidNotOnOrAfter"
value=${provider?.sessionValidNotOnOrAfter || "minutes=86400"}
required
label=${msg("Session valid not on or after")}
help=${msg("Session not valid on or after current time + this value.")}
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
></ak-text-input>
<ak-radio-input
name="digestAlgorithm"
label=${msg("Digest algorithm")}
required
.options=${digestAlgorithmOptions}
.value=${provider?.digestAlgorithm}
>
</ak-radio-input>
<ak-radio-input
name="signatureAlgorithm"
label=${msg("Signature algorithm")}
required
.options=${signatureAlgorithmOptions}
.value=${provider?.signatureAlgorithm}
>
</ak-radio-input>
</div>
</ak-form-group>
${renderForm(
(this.wizard.provider as SAMLProvider) ?? {},
this.wizard.errors.provider,
setHasSigningKp,
this.hasSigningKp,
)}
</form>`;
}
}

View File

@ -1,21 +1,10 @@
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
import "@goauthentik/admin/common/ak-core-group-search";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-multi-select";
import "@goauthentik/components/ak-switch-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
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";
import { ifDefined } from "lit/directives/if-defined.js";
import { PaginatedSCIMMappingList, PropertymappingsApi, type SCIMProvider } from "@goauthentik/api";
import { PaginatedSCIMMappingList, type SCIMProvider } from "@goauthentik/api";
import BaseProviderPanel from "../BaseProviderPanel";
@ -26,125 +15,15 @@ export class ApplicationWizardAuthenticationBySCIM extends BaseProviderPanel {
constructor() {
super();
new PropertymappingsApi(DEFAULT_CONFIG)
.propertymappingsProviderScimList({
ordering: "managed",
})
.then((propertyMappings: PaginatedSCIMMappingList) => {
this.propertyMappings = propertyMappings;
});
}
propertyMappingConfiguration(provider?: SCIMProvider) {
const propertyMappings = this.propertyMappings?.results ?? [];
const configuredMappings = (providerMappings: string[]) =>
propertyMappings.map((pm) => pm.pk).filter((pmpk) => providerMappings.includes(pmpk));
const managedMappings = (key: string) =>
propertyMappings
.filter((pm) => pm.managed === `goauthentik.io/providers/scim/${key}`)
.map((pm) => pm.pk);
const pmUserValues = provider?.propertyMappings
? configuredMappings(provider?.propertyMappings ?? [])
: managedMappings("user");
const pmGroupValues = provider?.propertyMappingsGroup
? configuredMappings(provider?.propertyMappingsGroup ?? [])
: managedMappings("group");
const propertyPairs = propertyMappings.map((pm) => [pm.pk, pm.name]);
return { pmUserValues, pmGroupValues, propertyPairs };
}
render() {
const provider = this.wizard.provider as SCIMProvider | undefined;
const errors = this.wizard.errors.provider;
const { pmUserValues, pmGroupValues, propertyPairs } =
this.propertyMappingConfiguration(provider);
return html`<ak-wizard-title>${msg("Configure SCIM Provider")}</ak-wizard-title>
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
required
help=${msg("SCIM base url, usually ends in /v2.")}
.errorMessages=${errors?.url ?? []}
>
</ak-text-input>
<ak-text-input
name="token"
label=${msg("Token")}
value="${first(provider?.token, "")}"
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
>
</ak-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
?checked=${first(provider?.excludeUsersServiceAccount, true)}
label=${msg("Exclude service accounts")}
></ak-switch-input>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-core-group-search
.group=${provider?.filterGroup}
></ak-core-group-search>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-multi-select
label=${msg("User Property Mappings")}
name="propertyMappings"
.options=${propertyPairs}
.values=${pmUserValues}
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for user mapping.")}
</p>
`}
></ak-multi-select>
<ak-multi-select
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
.options=${propertyPairs}
.values=${pmGroupValues}
.richhelp=${html`
<p class="pf-c-form__helper-text">
${msg("Property mappings used for group creation.")}
</p>
`}
></ak-multi-select>
</div>
</ak-form-group>
${renderForm(
(this.wizard.provider as SCIMProvider) ?? {},
this.wizard.errors.provider,
)}
</form>`;
}
}

View File

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

View File

@ -5,8 +5,8 @@ import "@goauthentik/elements/forms/SearchSelect";
import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter";
import { html } from "lit";
import { customElement } from "lit/decorators.js";
import { property, query } from "lit/decorators.js";
import { customElement, property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CertificateKeyPair,
@ -114,6 +114,7 @@ export class AkCryptoCertificateSearch extends CustomListenerElement(AKElement)
render() {
return html`
<ak-search-select
name=${ifDefined(this.name ?? undefined)}
.fetchObjects=${this.fetchObjects}
.renderElement=${renderElement}
.value=${renderValue}

View File

@ -7,6 +7,7 @@ import { CustomListenerElement } from "@goauthentik/elements/utils/eventEmitter"
import { html } from "lit";
import { property, query } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import { FlowsApi, FlowsInstancesListDesignationEnum } from "@goauthentik/api";
import type { Flow, FlowsInstancesListRequest } from "@goauthentik/api";
@ -133,7 +134,7 @@ export class FlowSearch<T extends Flow> extends CustomListenerElement(AKElement)
.renderElement=${renderElement}
.renderDescription=${renderDescription}
.value=${getFlowValue}
.name=${this.name}
name=${ifDefined(this.name ?? undefined)}
@ak-change=${this.handleSearchUpdate}
?blankable=${!this.required}
>

View File

@ -43,8 +43,11 @@ 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) => {
this.providerTypes = providerTypes;
});
}
render(): TemplateResult {
@ -58,6 +61,7 @@ export class ProviderWizard extends AKElement {
}}
>
<ak-wizard-page-type-create
name="selectProviderType"
slot="initial"
layout=${TypeCreateWizardPageLayouts.grid}
.types=${this.providerTypes}

View File

@ -2,24 +2,17 @@ import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
LDAPAPIAccessMode,
LDAPProvider,
ProvidersApi,
} from "@goauthentik/api";
import { LDAPProvider, ProvidersApi } from "@goauthentik/api";
import { renderForm } from "./LDAPProviderFormForm.js";
@customElement("ak-provider-ldap-form")
export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPProvider>) {
@ -42,212 +35,8 @@ export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPP
}
}
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
// Authentication field. So, yeah, we're using the authorization field to store the
// authentication information, which is why the ak-branded-flow-search call down there looks so
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
// field of the target Provider.
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("Bind mode")} name="bindMode">
<ak-radio
.options=${[
{
label: msg("Cached binding"),
value: LDAPAPIAccessMode.Cached,
default: true,
description: html`${msg(
"Flow is executed and session is cached in memory. Flow is executed when session expires",
)}`,
},
{
label: msg("Direct binding"),
value: LDAPAPIAccessMode.Direct,
description: html`${msg(
"Always execute the configured bind flow to authenticate the user",
)}`,
},
]}
.value=${this.instance?.bindMode}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Configure how the outpost authenticates requests.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Search mode")} name="searchMode">
<ak-radio
.options=${[
{
label: msg("Cached querying"),
value: LDAPAPIAccessMode.Cached,
default: true,
description: html`${msg(
"The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
)}`,
},
{
label: msg("Direct querying"),
value: LDAPAPIAccessMode.Direct,
description: html`${msg(
"Always returns the latest data, but slower than cached querying",
)}`,
},
]}
.value=${this.instance?.searchMode}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Configure how the outpost queries the core authentik server's users.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="mfaSupport">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.mfaSupport, 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("Code-based MFA Support")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Bind flow")}
name="authorizationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authorizationFlow}
.brandFlow=${this.brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Unbind flow")}
name="invalidationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
.brandFlow=${this.brand.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for unbinding users.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Base DN")}
?required=${true}
name="baseDn"
>
<input
type="text"
value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
<ak-crypto-certificate-search
.certificate=${this.instance?.certificate}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("TLS Server name")}
name="tlsServerName"
>
<input
type="text"
value="${first(this.instance?.tlsServerName, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("UID start number")}
?required=${true}
name="uidStartNumber"
>
<input
type="number"
value="${first(this.instance?.uidStartNumber, 2000)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("GID start number")}
?required=${true}
name="gidStartNumber"
>
<input
type="number"
value="${first(this.instance?.gidStartNumber, 4000)}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
renderForm() {
return renderForm(this.instance ?? {}, [], this.brand);
}
}

View File

@ -0,0 +1,178 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import "@goauthentik/components/ak-number-input";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CurrentBrand,
FlowsInstancesListDesignationEnum,
LDAPProvider,
ValidationError,
} from "@goauthentik/api";
import {
bindModeOptions,
cryptoCertificateHelp,
gidStartNumberHelp,
mfaSupportHelp,
searchModeOptions,
tlsServerNameHelp,
uidStartNumberHelp,
} from "./LDAPOptionsAndHelp.js";
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. LDAP needs only one field, but it is not an Authorization field, it is an Authentication
// field. So, yeah, we're using the authorization field to store the authentication information,
// which is why the ak-branded-flow-search call down there looks so weird-- we're looking up
// Authentication flows, but we're storing them in the Authorization field of the target Provider.
export function renderForm(
provider?: Partial<LDAPProvider>,
errors: ValidationError = {},
brand?: CurrentBrand,
) {
return html`
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-radio-input
label=${msg("Bind mode")}
name="bindMode"
.options=${bindModeOptions}
.value=${provider?.bindMode}
help=${msg("Configure how the outpost authenticates requests.")}
>
</ak-radio-input>
<ak-radio-input
label=${msg("Search mode")}
name="searchMode"
.options=${searchModeOptions}
.value=${provider?.searchMode}
help=${msg("Configure how the outpost queries the core authentik server's users.")}
>
</ak-radio-input>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Bind flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used for users to authenticate.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Unbind flow")}
name="invalidationFlow"
required
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.brandFlow=${brand?.flowInvalidation}
defaultFlowSlug="default-invalidation-flow"
.errorMessages=${errors?.invalidationFlow ?? []}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for unbinding users.")}</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="baseDn"
label=${msg("Base DN")}
required
value="${provider?.baseDn ?? "DC=ldap,DC=goauthentik,DC=io"}"
.errorMessages=${errors?.baseDn ?? []}
help=${msg(
"LDAP DN under which bind requests and search requests can be made.",
)}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Certificate")}
name="certificate"
.errorMessages=${errors?.certificate ?? []}
>
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.certificate ?? nothing)}
name="certificate"
>
</ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${cryptoCertificateHelp}</p>
</ak-form-element-horizontal>
<ak-text-input
label=${msg("TLS Server name")}
name="tlsServerName"
value="${provider?.tlsServerName ?? ""}"
.errorMessages=${errors?.tlsServerName ?? []}
help=${tlsServerNameHelp}
></ak-text-input>
<ak-number-input
label=${msg("UID start number")}
required
name="uidStartNumber"
value="${provider?.uidStartNumber ?? 2000}"
.errorMessages=${errors?.uidStartNumber ?? []}
help=${uidStartNumberHelp}
></ak-number-input>
<ak-number-input
label=${msg("GID start number")}
required
name="gidStartNumber"
value="${provider?.gidStartNumber ?? 4000}"
.errorMessages=${errors?.gidStartNumber ?? []}
help=${gidStartNumberHelp}
></ak-number-input>
</div>
</ak-form-group>
`;
}

View File

@ -1,12 +1,5 @@
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 {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
@ -19,105 +12,12 @@ import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { TemplateResult, css, html } from "lit";
import { css } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
IssuerModeEnum,
MatchingModeEnum,
OAuth2Provider,
ProvidersApi,
RedirectURI,
SubModeEnum,
} from "@goauthentik/api";
import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "./OAuth2PropertyMappings.js";
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [
{
label: msg("Confidential"),
value: ClientTypeEnum.Confidential,
default: true,
description: html`${msg(
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
)}`,
},
{
label: msg("Public"),
value: ClientTypeEnum.Public,
description: html`${msg(
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
)}`,
},
];
export const subjectModeOptions = [
{
label: msg("Based on the User's hashed ID"),
value: SubModeEnum.HashedUserId,
default: true,
},
{
label: msg("Based on the User's ID"),
value: SubModeEnum.UserId,
},
{
label: msg("Based on the User's UUID"),
value: SubModeEnum.UserUuid,
},
{
label: msg("Based on the User's username"),
value: SubModeEnum.UserUsername,
},
{
label: msg("Based on the User's Email"),
value: SubModeEnum.UserEmail,
description: html`${msg("This is recommended over the UPN mode.")}`,
},
{
label: msg("Based on the User's UPN"),
value: SubModeEnum.UserUpn,
description: html`${msg(
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
)}`,
},
];
export const issuerModeOptions = [
{
label: msg("Each provider has a different issuer, based on the application slug"),
value: IssuerModeEnum.PerProvider,
default: true,
},
{
label: msg("Same identifier is used for all providers"),
value: IssuerModeEnum.Global,
},
];
const redirectUriHelpMessages = [
msg(
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
),
msg(
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
),
msg(
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
),
];
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
import { renderForm } from "./OAuth2ProviderFormForm.js";
/**
* Form page for OAuth2 Authentication Method
@ -131,9 +31,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
@state()
showClientSecret = true;
@state()
redirectUris: RedirectURI[] = [];
static get styles() {
return super.styles.concat(css`
ak-array-input {
@ -147,7 +44,6 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
id: pk,
});
this.showClientSecret = provider.clientType === ClientTypeEnum.Confidential;
this.redirectUris = provider.redirectUris;
return provider;
}
@ -164,245 +60,11 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
}
}
renderForm(): TemplateResult {
const provider = this.instance;
return html` <ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
?required=${true}
>
<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>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
.value=${provider?.clientType}
required
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
this.showClientSecret = ev.detail.value !== ClientTypeEnum.Public;
}}
.options=${clientTypeOptions}
>
</ak-radio-input>
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value="${first(
provider?.clientId,
randomString(40, ascii_letters + digits),
)}"
required
>
</ak-text-input>
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value="${first(
provider?.clientSecret,
randomString(128, ascii_letters + digits),
)}"
?hidden=${!this.showClientSecret}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Redirect URIs/Origins")}
required
name="redirectUris"
>
<ak-array-input
.items=${this.instance?.redirectUris ?? []}
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
.row=${(f?: RedirectURI) =>
akOAuthRedirectURIInput({
".redirectURI": f,
"style": "width: 100%",
"name": "oauth2-redirect-uri",
} as unknown as IRedirectURIInput)}
>
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(this.instance?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("Key used to encrypt the tokens.")}
</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
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<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>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
required
value="${first(provider?.accessCodeValidity, "minutes=1")}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${first(provider?.accessTokenValidity, "minutes=5")}"
required
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long access tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${first(provider?.refreshTokenValidity, "days=30")}"
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long refresh tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${oauth2PropertyMappingsProvider}
.selector=${makeOAuth2PropertyMappingsSelector(
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(
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
)}
</p>
</ak-form-element-horizontal>
<ak-radio-input
name="subMode"
label=${msg("Subject mode")}
required
.options=${subjectModeOptions}
.value=${provider?.subMode}
help=${msg(
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
)}
>
</ak-radio-input>
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${first(provider?.includeClaimsInIdToken, true)}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}
></ak-switch-input>
<ak-radio-input
name="issuerMode"
label=${msg("Issuer mode")}
required
.options=${issuerModeOptions}
.value=${provider?.issuerMode}
help=${msg(
"Configure how the issuer field of the ID Token should be filled.",
)}
>
</ak-radio-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<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>`;
renderForm() {
const showClientSecretCallback = (show: boolean) => {
this.showClientSecret = show;
};
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
}
}

View File

@ -0,0 +1,350 @@
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import {
IRedirectURIInput,
akOAuthRedirectURIInput,
} from "@goauthentik/admin/providers/oauth2/OAuth2ProviderRedirectURI";
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
import "@goauthentik/components/ak-radio-input";
import "@goauthentik/components/ak-text-input";
import "@goauthentik/components/ak-textarea-input";
import "@goauthentik/elements/ak-array-input.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
ClientTypeEnum,
FlowsInstancesListDesignationEnum,
IssuerModeEnum,
MatchingModeEnum,
OAuth2Provider,
RedirectURI,
SubModeEnum,
ValidationError,
} from "@goauthentik/api";
import {
makeOAuth2PropertyMappingsSelector,
oauth2PropertyMappingsProvider,
} from "./OAuth2PropertyMappings.js";
import { makeSourceSelector, oauth2SourcesProvider } from "./OAuth2Sources.js";
export const clientTypeOptions = [
{
label: msg("Confidential"),
value: ClientTypeEnum.Confidential,
default: true,
description: html`${msg(
"Confidential clients are capable of maintaining the confidentiality of their credentials such as client secrets",
)}`,
},
{
label: msg("Public"),
value: ClientTypeEnum.Public,
description: html`${msg(
"Public clients are incapable of maintaining the confidentiality and should use methods like PKCE. ",
)}`,
},
];
export const subjectModeOptions = [
{
label: msg("Based on the User's hashed ID"),
value: SubModeEnum.HashedUserId,
default: true,
},
{
label: msg("Based on the User's ID"),
value: SubModeEnum.UserId,
},
{
label: msg("Based on the User's UUID"),
value: SubModeEnum.UserUuid,
},
{
label: msg("Based on the User's username"),
value: SubModeEnum.UserUsername,
},
{
label: msg("Based on the User's Email"),
value: SubModeEnum.UserEmail,
description: html`${msg("This is recommended over the UPN mode.")}`,
},
{
label: msg("Based on the User's UPN"),
value: SubModeEnum.UserUpn,
description: html`${msg(
"Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.",
)}`,
},
];
export const issuerModeOptions = [
{
label: msg("Each provider has a different issuer, based on the application slug"),
value: IssuerModeEnum.PerProvider,
default: true,
},
{
label: msg("Same identifier is used for all providers"),
value: IssuerModeEnum.Global,
},
];
const redirectUriHelpMessages = [
msg(
"Valid redirect URIs after a successful authorization flow. Also specify any origins here for Implicit flows.",
),
msg(
"If no explicit redirect URIs are specified, the first successfully used redirect URI will be saved.",
),
msg(
'To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.',
),
];
export const redirectUriHelp = html`${redirectUriHelpMessages.map(
(m) => html`<p class="pf-c-form__helper-text">${m}</p>`,
)}`;
type ShowClientSecret = (show: boolean) => void;
const defaultShowClientSecret: ShowClientSecret = (_show) => undefined;
export function renderForm(
provider: Partial<OAuth2Provider>,
errors: ValidationError,
showClientSecret = false,
showClientSecretCallback: ShowClientSecret = defaultShowClientSecret,
) {
return html` <ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
required
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
?required=${true}
>
<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>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-radio-input
name="clientType"
label=${msg("Client type")}
.value=${provider?.clientType}
required
@change=${(ev: CustomEvent<{ value: ClientTypeEnum }>) => {
showClientSecretCallback(ev.detail.value !== ClientTypeEnum.Public);
}}
.options=${clientTypeOptions}
>
</ak-radio-input>
<ak-text-input
name="clientId"
label=${msg("Client ID")}
value="${provider?.clientId ?? randomString(40, ascii_letters + digits)}"
required
>
</ak-text-input>
<ak-text-input
name="clientSecret"
label=${msg("Client Secret")}
value="${provider?.clientSecret ?? randomString(128, ascii_letters + digits)}"
?hidden=${!showClientSecret}
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Redirect URIs/Origins")}
required
name="redirectUris"
>
<ak-array-input
name="redirectUris"
.items=${provider?.redirectUris ?? []}
.newItem=${() => ({ matchingMode: MatchingModeEnum.Strict, url: "" })}
.row=${(f?: RedirectURI) =>
akOAuthRedirectURIInput({
".redirectURI": f,
"style": "width: 100%",
"name": "oauth2-redirect-uri",
} as unknown as IRedirectURIInput)}
>
</ak-array-input>
${redirectUriHelp}
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Signing Key")} name="signingKey">
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.signingKey ?? undefined)}
singleton
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to sign the tokens.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Encryption Key")} name="encryptionKey">
<!-- NOTE: 'null' cast to 'undefined' on encryptionKey to satisfy Lit requirements -->
<ak-crypto-certificate-search
certificate=${ifDefined(provider?.encryptionKey ?? undefined)}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">${msg("Key used to encrypt the tokens.")}</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
name="authenticationFlow"
label=${msg("Authentication flow")}
>
<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>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="accessCodeValidity"
label=${msg("Access code validity")}
required
value="${provider?.accessCodeValidity ?? "minutes=1"}"
.bighelp=${html`<p class="pf-c-form__helper-text">
${msg("Configure how long access codes are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="accessTokenValidity"
label=${msg("Access Token validity")}
value="${provider?.accessTokenValidity ?? "minutes=5"}"
required
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long access tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-text-input
name="refreshTokenValidity"
label=${msg("Refresh Token validity")}
value="${provider?.refreshTokenValidity ?? "days=30"}"
?required=${true}
.bighelp=${html` <p class="pf-c-form__helper-text">
${msg("Configure how long refresh tokens are valid for.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>`}
>
</ak-text-input>
<ak-form-element-horizontal label=${msg("Scopes")} name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${oauth2PropertyMappingsProvider}
.selector=${makeOAuth2PropertyMappingsSelector(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(
"Select which scopes can be used by the client. The client still has to specify the scope to access the data.",
)}
</p>
</ak-form-element-horizontal>
<ak-radio-input
name="subMode"
label=${msg("Subject mode")}
required
.options=${subjectModeOptions}
.value=${provider?.subMode}
help=${msg(
"Configure what data should be used as unique User Identifier. For most cases, the default should be fine.",
)}
>
</ak-radio-input>
<ak-switch-input
name="includeClaimsInIdToken"
label=${msg("Include claims in id_token")}
?checked=${provider?.includeClaimsInIdToken ?? true}
help=${msg(
"Include User claims from scopes in the id_token, for applications that don't access the userinfo endpoint.",
)}
></ak-switch-input>
<ak-radio-input
name="issuerMode"
label=${msg("Issuer mode")}
required
.options=${issuerModeOptions}
.value=${provider?.issuerMode}
help=${msg("Configure how the issuer field of the ID Token should be filled.")}
>
</ak-radio-input>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header">${msg("Machine-to-Machine authentication settings")}</span>
<div slot="body" class="pf-c-form">
<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>`;
}

View File

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

View File

@ -0,0 +1,343 @@
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,
ValidationError,
} from "@goauthentik/api";
import {
makeProxyPropertyMappingsSelector,
proxyPropertyMappingsProvider,
} from "./ProxyProviderPropertyMappings.js";
export type ProxyModeValue = { value: ProxyMode };
export type SetMode = (ev: CustomEvent<ProxyModeValue>) => void;
export type SetShowHttpBasic = (ev: Event) => void;
export interface ProxyModeExtraArgs {
mode: ProxyMode;
onSetMode: SetMode;
showHttpBasic: boolean;
onSetShowHttpBasic: SetShowHttpBasic;
}
function renderHttpBasic(provider: Partial<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: Partial<ProxyProvider>, errors?: ValidationError) {
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-text-input
name="externalHost"
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.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"
label=${msg("Internal host")}
value="${ifDefined(provider?.internalHost)}"
required
.errorMessages=${errors?.internalHost ?? []}
help=${msg("Upstream host that the requests are forwarded to.")}
></ak-text-input>
<ak-switch-input
name="internalHostSslValidation"
label=${msg("Internal host SSL Validation")}
?checked=${provider?.internalHostSslValidation ?? true}
help=${msg("Validate SSL Certificates of upstream servers.")}
>
</ak-switch-input>`;
}
function renderForwardSingleSettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
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-text-input
name="externalHost"
label=${msg("External host")}
value="${ifDefined(provider?.externalHost)}"
required
.errorMessages=${errors?.externalHost ?? []}
help=${msg(
"The external URL you'll access the application at. Include any non-standard port.",
)}
></ak-text-input>`;
}
function renderForwardDomainSettings(provider: Partial<ProxyProvider>, errors?: ValidationError) {
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-text-input
name="externalHost"
label=${msg("Authentication URL")}
value="${provider?.externalHost ?? window.location.origin}"
required
.errorMessages=${errors?.externalHost ?? []}
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
label=${msg("Cookie domain")}
name="cookieDomain"
value="${ifDefined(provider?.cookieDomain)}"
required
.errorMessages=${errors?.cookieDomain ?? []}
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> `;
}
type StrictProxyMode = Omit<ProxyMode, "11184809">;
function renderSettings(provider: Partial<ProxyProvider>, mode: ProxyMode) {
return match(mode as StrictProxyMode)
.with(ProxyMode.Proxy, () => renderProxySettings(provider))
.with(ProxyMode.ForwardSingle, () => renderForwardSingleSettings(provider))
.with(ProxyMode.ForwardDomain, () => renderForwardDomainSettings(provider))
.otherwise(() => {
throw new Error("Unrecognized proxy mode");
});
}
export function renderForm(
provider: Partial<ProxyProvider> = {},
errors: ValidationError = {},
args: ProxyModeExtraArgs,
) {
const { mode, onSetMode, showHttpBasic, onSetShowHttpBasic } = args;
return html`
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
></ak-text-input>
<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-text-input
label=${msg("Token validity")}
name="accessTokenValidity"
value="${provider?.accessTokenValidity ?? "hours=24"}"
.errorMessages=${errors?.accessTokenValidity ?? []}
required
.help=${msg("Configure how long tokens are valid for.")}
></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">
<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-switch-input
name="interceptHeaderAuth"
label=${msg("Intercept header authentication")}
?checked=${provider?.interceptHeaderAuth ?? true}
help=${msg(
"When enabled, authentik will intercept the Authorization header to authenticate the request.",
)}
>
</ak-switch-input>
<ak-switch-input
name="basicAuthEnabled"
label=${msg("Send HTTP-Basic Authentication")}
?checked=${provider?.basicAuthEnabled ?? false}
help=${msg(
"Send a custom HTTP-Basic Authentication header based on values from authentik.",
)}
@change=${onSetShowHttpBasic}
>
</ak-switch-input>
${showHttpBasic ? renderHttpBasic(provider) : nothing}
<ak-form-element-horizontal label=${msg("Trusted OIDC Sources")} name="jwksSources">
<ak-dual-select-dynamic-selected
.provider=${oauth2SourcesProvider}
.selector=${makeSourceSelector(provider?.jwksSources)}
available-label=${msg("Available Sources")}
selected-label=${msg("Selected Sources")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"JWTs signed by certificates configured in the selected sources can be used to authenticate to this provider.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group>
<span slot="header"> ${msg("Advanced flow settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Authentication flow")}
name="authenticationFlow"
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authenticationFlow}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg(
"Flow used when a user access this provider and is not authenticated.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
defaultFlowSlug="default-provider-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -1,48 +1,12 @@
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { ifDefined } from "lit-html/directives/if-defined.js";
import { customElement } from "lit/decorators.js";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
ProvidersApi,
RadiusProvider,
RadiusProviderPropertyMapping,
} from "@goauthentik/api";
import { ProvidersApi, RadiusProvider } from "@goauthentik/api";
export async function radiusPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderRadiusList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
}
import { renderForm } from "./RadiusProviderFormForm.js";
@customElement("ak-provider-radius-form")
export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<RadiusProvider>) {
@ -65,127 +29,8 @@ export class RadiusProviderFormPage extends WithBrandConfig(BaseProviderForm<Rad
}
}
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. Radius needs only one field, but it is not the Authorization field, it is an
// Authentication field. So, yeah, we're using the authorization field to store the
// authentication information, which is why the ak-branded-flow-search call down there looks so
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
// field of the target Provider.
renderForm(): TemplateResult {
return html`
<ak-form-element-horizontal label=${msg("Name")} required 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("Authentication flow")}
required
name="authorizationFlow"
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${this.instance?.authorizationFlow}
.brandFlow=${this.brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="mfaSupport">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.mfaSupport, 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("Code-based MFA Support")}</span>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Shared secret")}
required
name="sharedSecret"
>
<input
type="text"
value="${first(
this.instance?.sharedSecret,
randomString(128, ascii_letters + digits),
)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Client Networks")}
required
name="clientNetworks"
>
<input
type="text"
value="${first(this.instance?.clientNetworks, "0.0.0.0/0, ::/0")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(`List of CIDRs (comma-seperated) that clients can connect from. A more specific
CIDR will match before a looser one. Clients connecting from a non-specified CIDR
will be dropped.`)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${radiusPropertyMappingsProvider}
.selector=${makeRadiusPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
</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("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${this.instance?.invalidationFlow}
defaultFlowSlug="default-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
>
`;
renderForm() {
return renderForm(this.instance ?? {}, [], this.brand);
}
}

View File

@ -0,0 +1,154 @@
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { ascii_letters, digits, first, randomString } from "@goauthentik/common/utils";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CurrentBrand,
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
RadiusProvider,
RadiusProviderPropertyMapping,
ValidationError,
} from "@goauthentik/api";
export async function radiusPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderRadiusList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeRadiusPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, _]: DualSelectPair<RadiusProviderPropertyMapping>) => [];
}
const mfaSupportHelp = msg(
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
);
const clientNetworksHelp = msg(
"List of CIDRs (comma-seperated) that clients can connect from. A more specific CIDR will match before a looser one. Clients connecting from a non-specified CIDR will be dropped.",
);
// All Provider objects have an Authorization flow, but not all providers have an Authentication
// flow. Radius needs only one field, but it is not the Authorization field, it is an
// Authentication field. So, yeah, we're using the authorization field to store the
// authentication information, which is why the ak-branded-flow-search call down there looks so
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
// field of the target Provider.
export function renderForm(
provider?: Partial<RadiusProvider>,
errors: ValidationError = {},
brand?: CurrentBrand,
) {
return html`
<ak-text-input
name="name"
label=${msg("Name")}
value=${ifDefined(provider?.name)}
.errorMessages=${errors?.name ?? []}
required
>
</ak-text-input>
<ak-form-element-horizontal
label=${msg("Authentication flow")}
?required=${true}
name="authorizationFlow"
.errorMessages=${errors?.authorizationFlow ?? []}
>
<ak-branded-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authentication}
.currentFlow=${provider?.authorizationFlow}
.brandFlow=${brand?.flowAuthentication}
required
></ak-branded-flow-search>
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
</ak-form-element-horizontal>
<ak-switch-input
name="mfaSupport"
label=${msg("Code-based MFA Support")}
?checked=${provider?.mfaSupport ?? true}
help=${mfaSupportHelp}
>
</ak-switch-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="sharedSecret"
label=${msg("Shared secret")}
.errorMessages=${errors?.sharedSecret ?? []}
value=${first(
provider?.sharedSecret,
randomString(128, ascii_letters + digits),
)}
required
></ak-text-input>
<ak-text-input
name="clientNetworks"
label=${msg("Client Networks")}
value=${first(provider?.clientNetworks, "0.0.0.0/0, ::/0")}
.errorMessages=${errors?.clientNetworks ?? []}
required
help=${clientNetworksHelp}
></ak-text-input>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${radiusPropertyMappingsProvider}
.selector=${makeRadiusPropertyMappingsSelector(provider?.propertyMappings)}
available-label=${msg("Available Property Mappings")}
selected-label=${msg("Selected Property Mappings")}
></ak-dual-select-dynamic-selected>
</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("Invalidation flow")}
name="invalidationFlow"
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
.currentFlow=${provider?.invalidationFlow}
.errorMessages=${errors?.invalidationFlow ?? []}
defaultFlowSlug="default-invalidation-flow"
required
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when logging out of this provider.")}
</p>
</ak-form-element-horizontal>
</div></ak-form-group
>
`;
}

View File

@ -1,58 +1,12 @@
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import AkCryptoCertificateSearch from "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { type AkCryptoCertificateSearch } from "@goauthentik/admin/common/ak-crypto-certificate-search";
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { TemplateResult, html, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
PropertymappingsProviderSamlListRequest,
ProvidersApi,
SAMLPropertyMapping,
SAMLProvider,
SpBindingEnum,
} from "@goauthentik/api";
import { ProvidersApi, SAMLProvider } from "@goauthentik/api";
export async function samlPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList({
ordering: "saml_name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SAMLPropertyMapping>) =>
mapping?.managed?.startsWith("goauthentik.io/providers/saml");
}
import { renderForm } from "./SAMLProviderFormForm.js";
@customElement("ak-provider-saml-form")
export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
@ -80,368 +34,14 @@ export class SAMLProviderFormPage extends BaseProviderForm<SAMLProvider> {
}
}
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>
renderForm() {
const setHasSigningKp = (ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
};
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("ACS URL")}
?required=${true}
name="acsUrl"
>
<input
type="text"
value="${ifDefined(this.instance?.acsUrl)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Issuer")}
?required=${true}
name="issuer"
>
<input
type="text"
value="${this.instance?.issuer || "authentik"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">${msg("Also known as EntityID.")}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Service Provider Binding")}
?required=${true}
name="spBinding"
>
<ak-radio
.options=${[
{
label: msg("Redirect"),
value: SpBindingEnum.Redirect,
default: true,
},
{
label: msg("Post"),
value: SpBindingEnum.Post,
},
]}
.value=${this.instance?.spBinding}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Audience")} name="audience">
<input
type="text"
value="${ifDefined(this.instance?.audience)}"
class="pf-c-form-control"
/>
</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>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("Signing Certificate")}
name="signingKp"
>
<ak-crypto-certificate-search
.certificate=${this.instance?.signingKp}
@input=${(ev: InputEvent) => {
const target = ev.target as AkCryptoCertificateSearch;
if (!target) return;
this.hasSigningKp = !!target.selectedKeypair;
}}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"Certificate used to sign outgoing Responses going to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
${this.hasSigningKp
? html` <ak-form-element-horizontal name="signAssertion">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.signAssertion, 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("Sign assertions")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="signResponse">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.signResponse, false)}
/>
<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("Sign responses")}</span
>
</label>
<p class="pf-c-form__helper-text">
${msg(
"When enabled, the assertion element of the SAML response will be signed.",
)}
</p>
</ak-form-element-horizontal>`
: nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
>
<ak-crypto-certificate-search
.certificate=${this.instance?.verificationKp}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Encryption Certificate")}
name="encryptionKp"
>
<ak-crypto-certificate-search
.certificate=${this.instance?.encryptionKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, assertions will be encrypted using this keypair.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${samlPropertyMappingsProvider}
.selector=${makeSAMLPropertyMappingsSelector(
this.instance?.propertyMappings,
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("NameID Property Mapping")}
name="nameIdMapping"
>
<ak-search-select
.fetchObjects=${async (
query?: string,
): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
};
if (query !== undefined) {
args.search = query;
}
const items = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList(args);
return items.results;
}}
.renderElement=${(item: SAMLPropertyMapping): string => {
return item.name;
}}
.value=${(
item: SAMLPropertyMapping | undefined,
): string | undefined => {
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return this.instance?.nameIdMapping === item.pk;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Assertion valid not before")}
?required=${true}
name="assertionValidNotBefore"
>
<input
type="text"
value="${this.instance?.assertionValidNotBefore || "minutes=-5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Configure the maximum allowed time drift for an assertion.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Assertion valid not on or after")}
?required=${true}
name="assertionValidNotOnOrAfter"
>
<input
type="text"
value="${this.instance?.assertionValidNotOnOrAfter || "minutes=5"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Assertion not valid on or after current time + this value.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Session valid not on or after")}
?required=${true}
name="sessionValidNotOnOrAfter"
>
<input
type="text"
value="${this.instance?.sessionValidNotOnOrAfter || "minutes=86400"}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("Session not valid on or after current time + this value.")}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Default relay state")}
?required=${true}
name="defaultRelayState"
>
<input
type="text"
value="${this.instance?.defaultRelayState || ""}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Digest algorithm")}
?required=${true}
name="digestAlgorithm"
>
<ak-radio
.options=${digestAlgorithmOptions}
.value=${this.instance?.digestAlgorithm}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Signature algorithm")}
?required=${true}
name="signatureAlgorithm"
>
<ak-radio
.options=${signatureAlgorithmOptions}
.value=${this.instance?.signatureAlgorithm}
>
</ak-radio>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
return renderForm(this.instance ?? {}, [], setHasSigningKp, this.hasSigningKp);
}
}

View File

@ -0,0 +1,330 @@
import {
digestAlgorithmOptions,
signatureAlgorithmOptions,
} from "@goauthentik/admin/applications/wizard/methods/saml/SamlProviderOptions";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import "@goauthentik/elements/utils/TimeDeltaHelp";
import { msg } from "@lit/localize";
import { html, nothing } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
FlowsInstancesListDesignationEnum,
PropertymappingsApi,
PropertymappingsProviderSamlListRequest,
SAMLPropertyMapping,
SAMLProvider,
SpBindingEnum,
ValidationError,
} from "@goauthentik/api";
export async function samlPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList({
ordering: "saml_name",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSAMLPropertyMappingsSelector(instanceMappings?: string[]) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SAMLPropertyMapping>) =>
mapping?.managed?.startsWith("goauthentik.io/providers/saml");
}
const serviceProviderBindingOptions = [
{
label: msg("Redirect"),
value: SpBindingEnum.Redirect,
default: true,
},
{
label: msg("Post"),
value: SpBindingEnum.Post,
},
];
function renderHasSigningKp(provider?: Partial<SAMLProvider>) {
return html` <ak-switch-input
name="signAssertion"
label=${msg("Sign assertions")}
?checked=${provider?.signAssertion ?? true}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>
<ak-switch-input
name="signResponse"
label=${msg("Sign responses")}
?checked=${provider?.signResponse ?? false}
help=${msg("When enabled, the assertion element of the SAML response will be signed.")}
>
</ak-switch-input>`;
}
export function renderForm(
provider: Partial<SAMLProvider> = {},
errors: ValidationError,
setHasSigningKp: (ev: InputEvent) => void,
hasSigningKp: boolean,
) {
return html` <ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
required
.errorMessages=${errors?.name ?? []}
></ak-text-input>
<ak-form-element-horizontal
name="authorizationFlow"
label=${msg("Authorization flow")}
required
>
<ak-flow-search
flowType=${FlowsInstancesListDesignationEnum.Authorization}
.currentFlow=${provider?.authorizationFlow}
required
.errorMessages=${errors?.authorizationFlow ?? []}
></ak-flow-search>
<p class="pf-c-form__helper-text">
${msg("Flow used when authorizing this provider.")}
</p>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="acsUrl"
label=${msg("ACS URL")}
value="${ifDefined(provider?.acsUrl)}"
required
.errorMessages=${errors?.acsUrl ?? []}
></ak-text-input>
<ak-text-input
label=${msg("Issuer")}
name="issuer"
value="${provider?.issuer || "authentik"}"
required
.errorMessages=${errors?.issuer ?? []}
help=${msg("Also known as EntityID.")}
></ak-text-input>
<ak-radio-input
label=${msg("Service Provider Binding")}
name="spBinding"
required
.options=${serviceProviderBindingOptions}
.value=${provider?.spBinding}
help=${msg(
"Determines how authentik sends the response back to the Service Provider.",
)}
>
</ak-radio-input>
<ak-text-input
name="audience"
label=${msg("Audience")}
value="${ifDefined(provider?.audience)}"
.errorMessages=${errors?.audience ?? []}
></ak-text-input>
</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=${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>
<ak-form-group>
<span slot="header"> ${msg("Advanced protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Signing Certificate")} name="signingKp">
<ak-crypto-certificate-search
.certificate=${provider?.signingKp}
@input=${setHasSigningKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"Certificate used to sign outgoing Responses going to the Service Provider.",
)}
</p>
</ak-form-element-horizontal>
${hasSigningKp ? renderHasSigningKp(provider) : nothing}
<ak-form-element-horizontal
label=${msg("Verification Certificate")}
name="verificationKp"
>
<ak-crypto-certificate-search
.certificate=${provider?.verificationKp}
nokey
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg(
"When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Encryption Certificate")}
name="encryptionKp"
>
<ak-crypto-certificate-search
.certificate=${provider?.encryptionKp}
></ak-crypto-certificate-search>
<p class="pf-c-form__helper-text">
${msg("When selected, assertions will be encrypted using this keypair.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Property mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${samlPropertyMappingsProvider}
.selector=${makeSAMLPropertyMappingsSelector(provider?.propertyMappings)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("NameID Property Mapping")}
name="nameIdMapping"
>
<ak-search-select
.fetchObjects=${async (query?: string): Promise<SAMLPropertyMapping[]> => {
const args: PropertymappingsProviderSamlListRequest = {
ordering: "saml_name",
};
if (query !== undefined) {
args.search = query;
}
const items = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderSamlList(args);
return items.results;
}}
.renderElement=${(item: SAMLPropertyMapping): string => {
return item.name;
}}
.value=${(item: SAMLPropertyMapping | undefined): string | undefined => {
return item?.pk;
}}
.selected=${(item: SAMLPropertyMapping): boolean => {
return provider?.nameIdMapping === item.pk;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg(
"Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected.",
)}
</p>
</ak-form-element-horizontal>
<ak-text-input
name="assertionValidNotBefore"
label=${msg("Assertion valid not before")}
value="${provider?.assertionValidNotBefore || "minutes=-5"}"
required
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Configure the maximum allowed time drift for an assertion.")}
></ak-text-input>
<ak-text-input
name="assertionValidNotOnOrAfter"
label=${msg("Assertion valid not on or after")}
value="${provider?.assertionValidNotOnOrAfter || "minutes=5"}"
required
.errorMessages=${errors?.assertionValidNotBefore ?? []}
help=${msg("Assertion not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="sessionValidNotOnOrAfter"
label=${msg("Session valid not on or after")}
value="${provider?.sessionValidNotOnOrAfter || "minutes=86400"}"
required
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg("Session not valid on or after current time + this value.")}
></ak-text-input>
<ak-text-input
name="defaultRelayState"
label=${msg("Default relay state")}
value="${provider?.defaultRelayState || ""}"
.errorMessages=${errors?.sessionValidNotOnOrAfter ?? []}
help=${msg(
"When using IDP-initiated logins, the relay state will be set to this value.",
)}
></ak-text-input>
<ak-radio-input
name="digestAlgorithm"
label=${msg("Digest algorithm")}
.options=${digestAlgorithmOptions}
.value=${provider?.digestAlgorithm}
required
>
</ak-radio-input>
<ak-radio-input
name="signatureAlgorithm"
label=${msg("Signature algorithm")}
.options=${signatureAlgorithmOptions}
.value=${provider?.signatureAlgorithm}
required
>
</ak-radio-input>
</div>
</ak-form-group>`;
}

View File

@ -1,53 +1,11 @@
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CoreApi,
CoreGroupsListRequest,
Group,
PropertymappingsApi,
ProvidersApi,
SCIMMapping,
SCIMProvider,
} from "@goauthentik/api";
import { ProvidersApi, SCIMProvider } from "@goauthentik/api";
export async function scimPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderScimList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSCIMPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelected: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
mapping?.managed === defaultSelected;
}
import { renderForm } from "./SCIMProviderFormForm.js";
@customElement("ak-provider-scim-form")
export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
@ -70,156 +28,8 @@ export class SCIMProviderFormPage extends BaseProviderForm<SCIMProvider> {
}
}
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-group .expanded=${true}>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("URL")} ?required=${true} name="url">
<input
type="text"
value="${first(this.instance?.url, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg("SCIM base url, usually ends in /v2.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal name="verifyCertificates">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.verifyCertificates, 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("Verify SCIM server's certificates")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Token")}
?required=${true}
name="token"
>
<input
type="text"
value="${first(this.instance?.token, "")}"
class="pf-c-form-control"
required
/>
<p class="pf-c-form__helper-text">
${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal name="excludeUsersServiceAccount">
<label class="pf-c-switch">
<input
class="pf-c-switch__input"
type="checkbox"
?checked=${first(this.instance?.excludeUsersServiceAccount, 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("Exclude service accounts")}</span
>
</label>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(
args,
);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === this.instance?.filterGroup;
}}
?blankable=${true}
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group ?expanded=${true}>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings">
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
this.instance?.propertyMappings,
"goauthentik.io/providers/scim/user",
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
</select>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup">
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
this.instance?.propertyMappingsGroup,
"goauthentik.io/providers/scim/group",
)}
available-label=${msg("Available Group Property Mappings")}
selected-label=${msg("Selected Group Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>`;
renderForm() {
return renderForm(this.instance ?? {}, []);
}
}

View File

@ -0,0 +1,173 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import "@goauthentik/elements/forms/SearchSelect";
import { msg } from "@lit/localize";
import { html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CoreApi,
CoreGroupsListRequest,
Group,
PropertymappingsApi,
SCIMMapping,
SCIMProvider,
ValidationError,
} from "@goauthentik/api";
export async function scimPropertyMappingsProvider(page = 1, search = "") {
const propertyMappings = await new PropertymappingsApi(
DEFAULT_CONFIG,
).propertymappingsProviderScimList({
ordering: "managed",
pageSize: 20,
search: search.trim(),
page,
});
return {
pagination: propertyMappings.pagination,
options: propertyMappings.results.map((m) => [m.pk, m.name, m.name, m]),
};
}
export function makeSCIMPropertyMappingsSelector(
instanceMappings: string[] | undefined,
defaultSelected: string,
) {
const localMappings = instanceMappings ? new Set(instanceMappings) : undefined;
return localMappings
? ([pk, _]: DualSelectPair) => localMappings.has(pk)
: ([_0, _1, _2, mapping]: DualSelectPair<SCIMMapping>) =>
mapping?.managed === defaultSelected;
}
export function renderForm(provider?: Partial<SCIMProvider>, errors: ValidationError = {}) {
return html`
<ak-text-input
name="name"
value=${ifDefined(provider?.name)}
label=${msg("Name")}
.errorMessages=${errors?.name ?? []}
required
help=${msg("Method's display Name.")}
></ak-text-input>
<ak-form-group expanded>
<span slot="header"> ${msg("Protocol settings")} </span>
<div slot="body" class="pf-c-form">
<ak-text-input
name="url"
label=${msg("URL")}
value="${first(provider?.url, "")}"
.errorMessages=${errors?.url ?? []}
required
help=${msg("SCIM base url, usually ends in /v2.")}
></ak-text-input>
<ak-switch-input
name="verifyCertificates"
label=${msg("Verify SCIM server's certificates")}
?checked=${provider?.verifyCertificates ?? true}
>
</ak-switch-input>
<ak-text-input
name="token"
label=${msg("Token")}
value="${provider?.token ?? ""}"
.errorMessages=${errors?.token ?? []}
required
help=${msg(
"Token to authenticate with. Currently only bearer authentication is supported.",
)}
></ak-text-input>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header">${msg("User filtering")}</span>
<div slot="body" class="pf-c-form">
<ak-switch-input
name="excludeUsersServiceAccount"
label=${msg("Exclude service accounts")}
?checked=${first(provider?.excludeUsersServiceAccount, true)}
>
</ak-switch-input>
<ak-form-element-horizontal label=${msg("Group")} name="filterGroup">
<ak-search-select
.fetchObjects=${async (query?: string): Promise<Group[]> => {
const args: CoreGroupsListRequest = {
ordering: "name",
includeUsers: false,
};
if (query !== undefined) {
args.search = query;
}
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
return groups.results;
}}
.renderElement=${(group: Group): string => {
return group.name;
}}
.value=${(group: Group | undefined): string | undefined => {
return group ? group.pk : undefined;
}}
.selected=${(group: Group): boolean => {
return group.pk === provider?.filterGroup;
}}
blankable
>
</ak-search-select>
<p class="pf-c-form__helper-text">
${msg("Only sync users within the selected group.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
<ak-form-group expanded>
<span slot="header"> ${msg("Attribute mapping")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("User Property Mappings")}
name="propertyMappings"
>
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
provider?.propertyMappings,
"goauthentik.io/providers/scim/user",
)}
available-label=${msg("Available User Property Mappings")}
selected-label=${msg("Selected User Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to user mapping.")}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Group Property Mappings")}
name="propertyMappingsGroup"
>
<ak-dual-select-dynamic-selected
.provider=${scimPropertyMappingsProvider}
.selector=${makeSCIMPropertyMappingsSelector(
provider?.propertyMappingsGroup,
"goauthentik.io/providers/scim/group",
)}
available-label=${msg("Available Group Property Mappings")}
selected-label=${msg("Selected Group Property Mappings")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg("Property mappings used to group creation.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}

View File

@ -44,37 +44,43 @@ export class FormGroup extends AKElement {
}
render(): TemplateResult {
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
<div class="pf-c-form__field-group-toggle">
<div class="pf-c-form__field-group-toggle-button">
<button
class="pf-c-button pf-m-plain"
type="button"
aria-expanded="${this.expanded}"
aria-label=${this.ariaLabel}
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-form__field-group-toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</button>
return html` <div class="pf-c-form">
<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
<div class="pf-c-form__field-group-toggle">
<div class="pf-c-form__field-group-toggle-button">
<button
class="pf-c-button pf-m-plain"
type="button"
aria-expanded="${this.expanded}"
aria-label=${this.ariaLabel}
@click=${() => {
this.expanded = !this.expanded;
}}
>
<span class="pf-c-form__field-group-toggle-icon">
<i class="fas fa-angle-right" aria-hidden="true"></i>
</span>
</button>
</div>
</div>
</div>
<div class="pf-c-form__field-group-header">
<div class="pf-c-form__field-group-header-main">
<div class="pf-c-form__field-group-header-title">
<div class="pf-c-form__field-group-header-title-text">
<slot name="header"></slot>
<div class="pf-c-form__field-group-header">
<div class="pf-c-form__field-group-header-main">
<div class="pf-c-form__field-group-header-title">
<div class="pf-c-form__field-group-header-title-text">
<slot name="header"></slot>
</div>
</div>
<div class="pf-c-form__field-group-header-description">
<slot name="description"></slot>
</div>
</div>
<div class="pf-c-form__field-group-header-description">
<slot name="description"></slot>
</div>
</div>
<slot
?hidden=${!this.expanded}
class="pf-c-form__field-group-body"
name="body"
></slot>
</div>
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
</div>`;
}
}

View File

@ -0,0 +1,225 @@
import { browser } from "@wdio/globals";
import { match } from "ts-pattern";
import { Key } from "webdriverio";
export async function doBlur(el: WebdriverIO.Element | ChainablePromiseElement) {
const element = await el;
browser.execute((element) => element.blur(), element);
}
export function tap<A>(a: A) {
console.log("TAP:", a);
return a;
}
const makeComparator = (value: string | RegExp) =>
typeof value === "string"
? (sample: string) => sample === value
: (sample: string) => value.test(sample);
export async function checkIsPresent(name: string) {
await expect(await $(name)).toBeDisplayed();
}
export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
const context = ctx ?? browser;
const button = await (async () => {
for await (const button of context.$$("button")) {
if ((await button.isDisplayed()) && (await button.getText()).indexOf(name) !== -1) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(`Unable to find button '${name}'`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function clickToggleGroup(name: string, value: string | RegExp) {
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`[data-ouid-component-name=${name}]`).$$(
".pf-c-toggle-group__button",
)) {
if (comparator(await button.$(".pf-c-toggle-group__text").getText())) {
return button;
}
}
})();
if (!(button && (await button?.isDisplayed()))) {
throw new Error(`Unable to locate toggle button ${name}:${value.toString()}`);
}
await button.scrollIntoView();
await button.click();
await doBlur(button);
}
export async function setFormGroup(name: string | RegExp, setting: "open" | "closed") {
const comparator = makeComparator(name);
const formGroup = await (async () => {
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())
) {
return group;
}
}
})();
if (!(formGroup && (await formGroup.isDisplayed()))) {
throw new Error(`Unable to find ak-form-group[name="${name}"]`);
}
await formGroup.scrollIntoView();
const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button");
await match([await toggle.getAttribute("aria-expanded"), setting])
.with(["false", "open"], async () => await toggle.click())
.with(["true", "closed"], async () => await toggle.click())
.otherwise(async () => {});
await doBlur(formGroup);
}
export async function setRadio(name: string, value: string | RegExp) {
const control = await $(`ak-radio[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const item = await (async () => {
for await (const item of control.$$("div.pf-c-radio")) {
if (comparator(await item.$(".pf-c-radio__label").getText())) {
return item;
}
}
})();
if (!(item && (await item.isDisplayed()))) {
throw new Error(`Unable to find a radio that matches ${name}:${value.toString()}`);
}
await item.scrollIntoView();
await item.click();
await doBlur(control);
}
export async function setSearchSelect(name: string, value: string | RegExp) {
const control = await (async () => {
try {
const control = await $(`ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
} catch (_e: any) {
const control = await $(`ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
if (!(control && (await control.isExisting()))) {
throw new Error(`Unable to find an ak-search-select variant matching ${name}}`);
}
// Find the search select input control and activate it.
const view = await control.$("ak-search-select-view");
const input = await view.$('input[type="text"]');
await input.scrollIntoView();
await input.click();
const comparator = makeComparator(value);
const button = await (async () => {
for await (const button of $(`div[data-managed-for*="${name}"]`)
.$("ak-list-select")
.$$("button")) {
if (comparator(await button.getText())) {
return button;
}
}
})();
if (!(button && (await button.isDisplayed()))) {
throw new Error(
`Unable to find an ak-search-select entry matching ${name}:${value.toString()}`,
);
}
await (await button).click();
await browser.keys(Key.Tab);
await doBlur(control);
}
export async function setTextInput(name: string, value: string) {
const control = await $(`input[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setTextareaInput(name: string, value: string) {
const control = await $(`textarea[name="${name}"]`);
await control.scrollIntoView();
await control.setValue(value);
await doBlur(control);
}
export async function setToggle(name: string, set: boolean) {
const toggle = await $(`input[name="${name}"]`);
await toggle.scrollIntoView();
await expect(await toggle.getAttribute("type")).toBe("checkbox");
const state = await toggle.isSelected();
if (set !== state) {
const control = await (await toggle.parentElement()).$(".pf-c-switch__toggle");
await control.click();
await doBlur(control);
}
}
export async function setTypeCreate(name: string, value: string | RegExp) {
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const comparator = makeComparator(value);
const card = await (async () => {
for await (const card of $("ak-wizard-page-type-create").$$(
'[data-ouid-component-type="ak-type-create-grid-card"]',
)) {
if (comparator(await card.$(".pf-c-card__title").getText())) {
return card;
}
}
})();
if (!(card && (await card.isDisplayed()))) {
throw new Error(`Unable to locate radio card ${name}:${value.toString()}`);
}
await card.scrollIntoView();
await card.click();
await doBlur(control);
}
export type TestInteraction =
| [typeof checkIsPresent, ...Parameters<typeof checkIsPresent>]
| [typeof clickButton, ...Parameters<typeof clickButton>]
| [typeof clickToggleGroup, ...Parameters<typeof clickToggleGroup>]
| [typeof setFormGroup, ...Parameters<typeof setFormGroup>]
| [typeof setRadio, ...Parameters<typeof setRadio>]
| [typeof setSearchSelect, ...Parameters<typeof setSearchSelect>]
| [typeof setTextInput, ...Parameters<typeof setTextInput>]
| [typeof setTextareaInput, ...Parameters<typeof setTextareaInput>]
| [typeof setToggle, ...Parameters<typeof setToggle>]
| [typeof setTypeCreate, ...Parameters<typeof setTypeCreate>];
export type TestSequence = TestInteraction[];
export type TestProvider = () => TestSequence;

View File

@ -1,4 +1,5 @@
import { browser } from "@wdio/globals";
import { match } from "ts-pattern";
import { Key } from "webdriverio";
const CLICK_TIME_DELAY = 250;
@ -7,6 +8,7 @@ const CLICK_TIME_DELAY = 250;
* Main page object containing all methods, selectors and functionality that is shared across all
* page objects
*/
export default class Page {
/**
* Opens a sub page of the page
@ -31,7 +33,6 @@ export default class Page {
* why it would be hard to simplify this further (`flow` vs `tentanted-flow` vs a straight-up
* SearchSelect each have different a `searchSelector`).
*/
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
const inputBind = await $(searchSelector);
const inputMain = await inputBind.$('input[type="text"]');
@ -55,6 +56,79 @@ export default class Page {
await browser.keys(Key.Tab);
}
async setSearchSelect(name: string, value: string) {
const control = await (async () => {
try {
const control = await $(`ak-search-select[name="${name}"]`);
await control.waitForExist({ timeout: 500 });
return control;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
} catch (_e: any) {
const control = await $(`ak-search-selects-ez[name="${name}"]`);
return control;
}
})();
// Find the search select input control and activate it.
const view = await control.$("ak-search-select-view");
const input = await view.$('input[type="text"]');
await input.scrollIntoView();
await input.click();
// Weirdly necessary because it's portals!
const searchBlock = await (
await $(`div[data-managed-for="${name}"]`).$("ak-list-select")
).shadow$$("button");
let target: WebdriverIO.Element;
// @ts-expect-error "Types break on shadow$$"
for (const button of searchBlock) {
if ((await button.getText()).includes(value)) {
target = button;
break;
}
}
// @ts-expect-error "TSC cannot tell if the `for` loop actually performs the assignment."
if (!target) {
throw new Error(`Expected to find an entry matching the spec ${value}`);
}
await (await target).click();
await browser.keys(Key.Tab);
}
async setTextInput(name: string, value: string) {
const control = await $(`input[name="${name}"}`);
await control.scrollIntoView();
await control.setValue(value);
}
async setRadio(name: string, value: string) {
const control = await $(`ak-radio[name="${name}"]`);
await control.scrollIntoView();
const item = await control.$(`label.*=${value}`).parentElement();
await item.scrollIntoView();
await item.click();
}
async setTypeCreate(name: string, value: string) {
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
await control.scrollIntoView();
const selection = await $(`.pf-c-card__.*=${value}`);
await selection.scrollIntoView();
await selection.click();
}
async setFormGroup(name: string, setting: "open" | "closed") {
const formGroup = await $(`ak-form-group span[slot="header"].*=${name}`).parentElement();
await formGroup.scrollIntoView();
const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button");
await match([await toggle.getAttribute("expanded"), setting])
.with(["false", "open"], async () => await toggle.click())
.with(["true", "closed"], async () => await toggle.click())
.otherwise(async () => {});
}
public async logout() {
await browser.url("http://localhost:9000/flows/-/default/invalidation/");
return await this.pause();

View File

@ -9,29 +9,50 @@ import ApplicationWizardView from "../pageobjects/application-wizard.page.js";
import ApplicationsListPage from "../pageobjects/applications-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
import {
completeForwardAuthDomainProxyProviderForm,
completeForwardAuthProxyProviderForm,
completeLDAPProviderForm,
completeOAuth2ProviderForm,
completeProxyProviderForm,
completeRadiusProviderForm,
completeSAMLProviderForm,
completeSCIMProviderForm,
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./provider-shared-sequences.js";
import { type TestSequence } from "./shared-sequences";
async function reachTheProvider(title: string) {
const newPrefix = randomId();
const SUCCESS_MESSAGE = "Your application has been saved";
async function reachTheApplicationsPage() {
await ApplicationsListPage.logout();
await login();
await ApplicationsListPage.open();
await ApplicationsListPage.pause("ak-page-header");
await expect(await ApplicationsListPage.pageHeader()).toBeDisplayed();
await expect(await ApplicationsListPage.pageHeader()).toHaveText("Applications");
}
async function fillOutTheApplication(title: string) {
const newPrefix = randomId();
await (await ApplicationsListPage.startWizardButton()).click();
await (await ApplicationWizardView.wizardTitle()).waitForDisplayed();
await expect(await ApplicationWizardView.wizardTitle()).toHaveText("New application");
await (await ApplicationWizardView.app.name()).setValue(`${title} - ${newPrefix}`);
await (await ApplicationWizardView.app.uiSettings()).scrollIntoView();
await (await ApplicationWizardView.app.uiSettings()).click();
await (await ApplicationWizardView.app.launchUrl()).scrollIntoView();
await (await ApplicationWizardView.app.launchUrl()).setValue("http://example.goauthentik.io");
await (await ApplicationWizardView.nextButton()).click();
return await ApplicationWizardView.pause();
await ApplicationWizardView.pause();
}
async function getCommitMessage() {
@ -39,136 +60,51 @@ async function getCommitMessage() {
return await ApplicationWizardView.successMessage();
}
const SUCCESS_MESSAGE = "Your application has been saved";
const EXPLICIT_CONSENT = "default-provider-authorization-explicit-consent";
async function fillOutTheProviderAndCommit(provider: TestSequence) {
// The wizard automagically provides a name. If it doesn't, that's a bug.
const wizardProvider = provider.filter((p) => p.length < 2 || p[1] !== "name");
await $("ak-wizard-page-type-create").waitForDisplayed();
for await (const field of wizardProvider) {
const thefunc = field[0];
const args = field.slice(1);
// @ts-expect-error "This is a pretty alien call; I'm not surprised Typescript hates it."
await thefunc.apply($, args);
}
describe("Configure Applications with the Application Wizard", () => {
it("Should configure a simple LDAP Application", async () => {
await reachTheProvider("New LDAP Application");
await $("ak-wizard-frame").$("footer button.pf-m-primary").click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
}
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.ldapProvider).scrollIntoView();
await (await ApplicationWizardView.ldapProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.ldap.setBindFlow("default-authentication-flow");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
async function itShouldConfigureApplicationsViaTheWizard(name: string, provider: TestSequence) {
it(`Should successfully configure an application with a ${name} provider`, async () => {
await reachTheApplicationsPage();
await fillOutTheApplication(name);
await fillOutTheProviderAndCommit(provider);
});
}
it("Should configure a simple Oauth2 Application", async () => {
await reachTheProvider("New Oauth2 Application");
const providers = [
["Simple LDAP", simpleLDAPProviderForm],
["Simple OAuth2", simpleOAuth2ProviderForm],
["Simple Radius", simpleRadiusProviderForm],
["Simple SAML", simpleSAMLProviderForm],
["Simple SCIM", simpleSCIMProviderForm],
["Simple Proxy", simpleProxyProviderForm],
["Simple Forward Auth (single)", simpleForwardAuthProxyProviderForm],
["Simple Forward Auth (domain)", simpleForwardAuthDomainProxyProviderForm],
["Complete OAuth2", completeOAuth2ProviderForm],
["Complete LDAP", completeLDAPProviderForm],
["Complete Radius", completeRadiusProviderForm],
["Complete SAML", completeSAMLProviderForm],
["Complete SCIM", completeSCIMProviderForm],
["Complete Proxy", completeProxyProviderForm],
["Complete Forward Auth (single)", completeForwardAuthProxyProviderForm],
["Complete Forward Auth (domain)", completeForwardAuthDomainProxyProviderForm],
];
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.oauth2Provider).scrollIntoView();
await (await ApplicationWizardView.oauth2Provider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.oauth.setAuthorizationFlow(EXPLICIT_CONSENT);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple SAML Application", async () => {
await reachTheProvider("New SAML Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.samlProvider).scrollIntoView();
await (await ApplicationWizardView.samlProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.saml.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.saml.acsUrl.setValue("http://example.com:8000/");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple SCIM Application", async () => {
await reachTheProvider("New SCIM Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.scimProvider).scrollIntoView();
await (await ApplicationWizardView.scimProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.scim.url.setValue("http://example.com:8000/");
await ApplicationWizardView.scim.token.setValue("a-very-basic-token");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Radius Application", async () => {
await reachTheProvider("New Radius Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.radiusProvider).scrollIntoView();
await (await ApplicationWizardView.radiusProvider).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.radius.setAuthenticationFlow("default-authentication-flow");
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Transparent Proxy Application", async () => {
await reachTheProvider("New Transparent Proxy Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.proxyProviderProxy).scrollIntoView();
await (await ApplicationWizardView.proxyProviderProxy).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.transparentProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.transparentProxy.externalHost.setValue(
"http://external.example.com",
);
await ApplicationWizardView.transparentProxy.internalHost.setValue(
"http://internal.example.com",
);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
it("Should configure a simple Forward Proxy Application", async () => {
await reachTheProvider("New Forward Proxy Application");
await (await ApplicationWizardView.providerList()).waitForDisplayed();
await (await ApplicationWizardView.proxyProviderForwardsingle).scrollIntoView();
await (await ApplicationWizardView.proxyProviderForwardsingle).click();
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await ApplicationWizardView.forwardProxy.setAuthorizationFlow(EXPLICIT_CONSENT);
await ApplicationWizardView.forwardProxy.externalHost.setValue(
"http://external.example.com",
);
await (await ApplicationWizardView.nextButton()).click();
await ApplicationWizardView.pause();
await expect(await getCommitMessage()).toHaveText(SUCCESS_MESSAGE);
});
describe("Configuring Applications Via the Wizard", () => {
for (const [name, provider] of providers) {
itShouldConfigureApplicationsViaTheWizard(name, provider());
}
});

View File

@ -1,47 +0,0 @@
import { expect } from "@wdio/globals";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
describe("Configure Oauth2 Providers", () => {
it("Should configure a simple LDAP Application", async () => {
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await $('div[data-ouid-component-name="oauth2provider"]').scrollIntoView();
await $('div[data-ouid-component-name="oauth2provider"]').click();
await ProviderWizardView.nextButton.click();
await ProviderWizardView.pause();
return await $('ak-form-element-horizontal[name="name"]').$("input");
await ProviderWizardView.oauth.setAuthorizationFlow(
"default-provider-authorization-explicit-consent",
);
await ProviderWizardView.nextButton.click();
await ProviderWizardView.pause();
await ProvidersListPage.searchInput.setValue(newProviderName);
await ProvidersListPage.clickSearchButton();
await ProvidersListPage.pause();
const newProvider = await ProvidersListPage.findProviderRow();
await newProvider.waitForDisplayed();
expect(newProvider).toExist();
expect(await newProvider.getText()).toHaveText(newProviderName);
});
});

View File

@ -0,0 +1,323 @@
import {
type TestProvider,
type TestSequence,
checkIsPresent,
clickButton,
clickToggleGroup,
setFormGroup,
setRadio,
setSearchSelect,
setTextInput,
setTextareaInput,
setToggle,
setTypeCreate,
} from "pageobjects/controls.js";
import { ascii_letters, digits, randomString } from "../utils";
import { randomId } from "../utils/index.js";
const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`;
// components.schemas.OAuth2ProviderRequest
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - client_type
// - client_id
// - client_secret
// - access_code_validity
// - access_token_validity
// - refresh_token_validity
// - include_claims_in_id_token
// - signing_key
// - encryption_key
// - redirect_uris
// - sub_mode
// - issuer_mode
// - jwks_sources
//
export const simpleOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
];
export const completeOAuth2ProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setFormGroup, /Protocol settings/, "open"],
[setRadio, "clientType", "Public"],
// Switch back so we can make sure `clientSecret` is available.
[setRadio, "clientType", "Confidential"],
[checkIsPresent, '[name="clientId"]'],
[checkIsPresent, '[name="clientSecret"]'],
[setSearchSelect, "signingKey", /authentik Self-signed Certificate/],
[setSearchSelect, "encryptionKey", /authentik Self-signed Certificate/],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setFormGroup, /Advanced protocol settings/, "open"],
[setTextInput, "accessCodeValidity", "minutes=2"],
[setTextInput, "accessTokenValidity", "minutes=10"],
[setTextInput, "refreshTokenValidity", "days=40"],
[setToggle, "includeClaimsInIdToken", false],
[checkIsPresent, '[name="redirectUris"]'],
[setRadio, "subMode", "Based on the User's username"],
[setRadio, "issuerMode", "Same identifier is used for all providers"],
[setFormGroup, /Machine-to-Machine authentication settings/, "open"],
[checkIsPresent, '[name="jwksSources"]'],
];
// components.schemas.LDAPProviderRequest
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - base_dn
// - certificate
// - tls_server_name
// - uid_start_number
// - gid_start_number
// - search_mode
// - bind_mode
// - mfa_support
//
export const simpleLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
// This will never not weird me out.
[setFormGroup, /Flow settings/, "open"],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
];
export const completeLDAPProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
[setFormGroup, /Flow settings/, "open"],
[setFormGroup, /Protocol settings/, "open"],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setTextInput, "baseDn", "DC=ldap-2,DC=goauthentik,DC=io"],
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
[checkIsPresent, '[name="tlsServerName"]'],
[setTextInput, "uidStartNumber", "2001"],
[setTextInput, "gidStartNumber", "4001"],
[setRadio, "searchMode", "Direct querying"],
[setRadio, "bindMode", "Direct binding"],
[setToggle, "mfaSupport", false],
];
export const simpleRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
];
export const completeRadiusProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", /default-authentication-flow/],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setFormGroup, /Protocol settings/, "open"],
[setToggle, "mfaSupport", false],
[setTextInput, "clientNetworks", ""],
[setTextInput, "clientNetworks", "0.0.0.0/0, ::/0"],
[setTextInput, "sharedSecret", randomString(128, ascii_letters + digits)],
[checkIsPresent, '[name="propertyMappings"]'],
];
// provider_components.schemas.SAMLProviderRequest.yml
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - acs_url
// - audience
// - issuer
// - assertion_valid_not_before
// - assertion_valid_not_on_or_after
// - session_valid_not_on_or_after
// - name_id_mapping
// - digest_algorithm
// - signature_algorithm
// - signing_kp
// - verification_kp
// - encryption_kp
// - sign_assertion
// - sign_response
// - sp_binding
// - default_relay_state
//
export const simpleSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setTextInput, "acsUrl", "http://example.com:8000/"],
];
export const completeSAMLProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SAML Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SAML Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[setTextInput, "acsUrl", "http://example.com:8000/"],
[setTextInput, "issuer", "someone-else"],
[setRadio, "spBinding", "Post"],
[setTextInput, "audience", ""],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setFormGroup, /Advanced protocol settings/, "open"],
[checkIsPresent, '[name="propertyMappings"]'],
[setSearchSelect, "signingKp", /authentik Self-signed Certificate/],
[setSearchSelect, "verificationKp", /authentik Self-signed Certificate/],
[setSearchSelect, "encryptionKp", /authentik Self-signed Certificate/],
[setSearchSelect, "nameIdMapping", /authentik default SAML Mapping. Username/],
[setTextInput, "assertionValidNotBefore", "minutes=-10"],
[setTextInput, "assertionValidNotOnOrAfter", "minutes=10"],
[setTextInput, "sessionValidNotOnOrAfter", "minutes=172800"],
[checkIsPresent, '[name="defaultRelayState"]'],
[setRadio, "digestAlgorithm", "SHA512"],
[setRadio, "signatureAlgorithm", "RSA-SHA512"],
// These are only available after the signingKp is defined.
[setToggle, "signAssertion", true],
[setToggle, "signResponse", true],
];
// provider_components.schemas.SCIMProviderRequest.yml
//
// - name
// - property_mappings
// - property_mappings_group
// - url
// - verify_certificates
// - token
// - exclude_users_service_account
// - filter_group
//
export const simpleSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setTextInput, "token", "insert-real-token-here"],
];
export const completeSCIMProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "SCIM Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New SCIM Provider")],
[setTextInput, "url", "http://example.com:8000/"],
[setToggle, "verifyCertificates", false],
[setTextInput, "token", "insert-real-token-here"],
[setFormGroup, /Protocol settings/, "open"],
[setFormGroup, /User filtering/, "open"],
[setToggle, "excludeUsersServiceAccount", false],
[setSearchSelect, "filterGroup", /authentik Admins/],
[setFormGroup, /Attribute mapping/, "open"],
[checkIsPresent, '[name="propertyMappings"]'],
[checkIsPresent, '[name="propertyMappingsGroup"]'],
];
// provider_components.schemas.ProxyProviderRequest.yml
//
// - name
// - authentication_flow
// - authorization_flow
// - invalidation_flow
// - property_mappings
// - internal_host
// - external_host
// - internal_host_ssl_validation
// - certificate
// - skip_path_regex
// - basic_auth_enabled
// - basic_auth_password_attribute
// - basic_auth_user_attribute
// - mode
// - intercept_header_auth
// - cookie_domain
// - jwks_sources
// - access_token_validity
// - refresh_token_validity
// - refresh_token_validity is not handled in any of our forms. On purpose.
// - internal_host_ssl_validation
// - only on ProxyMode
export const simpleProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Proxy Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Proxy"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "internalHost", "http://example.com:8001/"],
];
export const simpleForwardAuthProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (single application)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
];
export const simpleForwardAuthDomainProxyProviderForm: TestProvider = () => [
[setTypeCreate, "selectProviderType", "Proxy Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Forward Auth Domain Level Provider")],
[setSearchSelect, "authorizationFlow", /default-provider-authorization-explicit-consent/],
[clickToggleGroup, "proxy-type-toggle", "Forward auth (domain level)"],
[setTextInput, "externalHost", "http://example.com:8000/"],
[setTextInput, "cookieDomain", "somedomain.tld"],
];
const proxyModeCompletions: TestSequence = [
[setTextInput, "accessTokenValidity", "hours=36"],
[setFormGroup, /Advanced protocol settings/, "open"],
[setSearchSelect, "certificate", /authentik Self-signed Certificate/],
[checkIsPresent, '[name="propertyMappings"]'],
[setTextareaInput, "skipPathRegex", "."],
[setFormGroup, /Authentication settings/, "open"],
[setToggle, "interceptHeaderAuth", false],
[setToggle, "basicAuthEnabled", true],
[setTextInput, "basicAuthUserAttribute", "authorized-user"],
[setTextInput, "basicAuthPasswordAttribute", "authorized-user-password"],
[setFormGroup, /Advanced flow settings/, "open"],
[setSearchSelect, "authenticationFlow", /default-source-authentication/],
[setSearchSelect, "invalidationFlow", /default-invalidation-flow/],
[checkIsPresent, '[name="jwksSources"]'],
];
export const completeProxyProviderForm: TestProvider = () => [
...simpleProxyProviderForm(),
[setToggle, "internalHostSslValidation", false],
...proxyModeCompletions,
];
export const completeForwardAuthProxyProviderForm: TestProvider = () => [
...simpleForwardAuthProxyProviderForm(),
...proxyModeCompletions,
];
export const completeForwardAuthDomainProxyProviderForm: TestProvider = () => [
...simpleForwardAuthProxyProviderForm(),
...proxyModeCompletions,
];

View File

@ -0,0 +1,98 @@
import { expect } from "@wdio/globals";
import { type TestProvider, type TestSequence } from "../pageobjects/controls";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { login } from "../utils/login.js";
import {
completeForwardAuthDomainProxyProviderForm,
completeForwardAuthProxyProviderForm,
completeLDAPProviderForm,
completeOAuth2ProviderForm,
completeProxyProviderForm,
completeRadiusProviderForm,
completeSAMLProviderForm,
completeSCIMProviderForm,
simpleForwardAuthDomainProxyProviderForm,
simpleForwardAuthProxyProviderForm,
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleProxyProviderForm,
simpleRadiusProviderForm,
simpleSAMLProviderForm,
simpleSCIMProviderForm,
} from "./provider-shared-sequences.js";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await expect(await containedMessages()).not.toContain("Successfully created provider.");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
const containedMessages = async () =>
await (async () => {
const messages = [];
for await (const alert of $("ak-message-container").$$("ak-message")) {
messages.push(await alert.$("p.pf-c-alert__title").getText());
}
return messages;
})();
const hasProviderSuccessMessage = async () =>
await browser.waitUntil(
async () => (await containedMessages()).includes("Successfully created provider."),
{ timeout: 1000, timeoutMsg: "Expected to see provider success message." },
);
async function fillOutFields(fields: TestSequence) {
for (const field of fields) {
const thefunc = field[0];
const args = field.slice(1);
// @ts-expect-error "This is a pretty alien call, so I'm not surprised Typescript doesn't like it."
await thefunc.apply($, args);
}
}
async function itShouldConfigureASimpleProvider(name: string, provider: TestSequence) {
it(`Should successfully configure a ${name} provider`, async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(provider);
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
}
type ProviderTest = [string, TestProvider];
describe("Configuring Providers", () => {
const providers: ProviderTest[] = [
["Simple LDAP", simpleLDAPProviderForm],
["Simple OAuth2", simpleOAuth2ProviderForm],
["Simple Radius", simpleRadiusProviderForm],
["Simple SAML", simpleSAMLProviderForm],
["Simple SCIM", simpleSCIMProviderForm],
["Simple Proxy", simpleProxyProviderForm],
["Simple Forward Auth (single application)", simpleForwardAuthProxyProviderForm],
["Simple Forward Auth (domain level)", simpleForwardAuthDomainProxyProviderForm],
["Complete OAuth2", completeOAuth2ProviderForm],
["Complete LDAP", completeLDAPProviderForm],
["Complete Radius", completeRadiusProviderForm],
["Complete SAML", completeSAMLProviderForm],
["Complete SCIM", completeSCIMProviderForm],
["Complete Proxy", completeProxyProviderForm],
["Complete Forward Auth (single application)", completeForwardAuthProxyProviderForm],
["Complete Forward Auth (domain level)", completeForwardAuthDomainProxyProviderForm],
];
for (const [name, provider] of providers) {
itShouldConfigureASimpleProvider(name, provider());
}
});

View File

@ -1,3 +1,22 @@
// Taken from python's string module
export const ascii_lowercase = "abcdefghijklmnopqrstuvwxyz";
export const ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const ascii_letters = ascii_lowercase + ascii_uppercase;
export const digits = "0123456789";
export const hexdigits = digits + "abcdef" + "ABCDEF";
export const octdigits = "01234567";
export const punctuation = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
export function randomString(len: number, charset: string): string {
const chars = [];
const array = new Uint8Array(len);
globalThis.crypto.getRandomValues(array);
for (let index = 0; index < len; index++) {
chars.push(charset[Math.floor(charset.length * (array[index] / Math.pow(2, 8)))]);
}
return chars.join("");
}
export function randomId() {
let dt = new Date().getTime();
return "xxxxxxxx".replace(/x/g, (c) => {