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.
This commit is contained in:
@ -1,37 +1,9 @@
|
|||||||
import "@goauthentik/admin/applications/wizard/ak-wizard-title";
|
import { renderForm } from "@goauthentik/admin/providers/oauth2/OAuth2ProviderFormForm.js";
|
||||||
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 {
|
|
||||||
makeSourceSelector,
|
|
||||||
oauth2SourcesProvider,
|
|
||||||
} from "@goauthentik/admin/providers/oauth2/OAuth2Sources.js";
|
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
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 { customElement, state } from "@lit/reactive-element/decorators.js";
|
||||||
import { html, nothing } from "lit";
|
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
|
||||||
|
|
||||||
import { ClientTypeEnum, FlowsInstancesListDesignationEnum, SourcesApi } from "@goauthentik/api";
|
import { SourcesApi } from "@goauthentik/api";
|
||||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||||
|
|
||||||
import BaseProviderPanel from "../BaseProviderPanel";
|
import BaseProviderPanel from "../BaseProviderPanel";
|
||||||
@ -59,227 +31,10 @@ export class ApplicationWizardAuthenticationByOauth extends BaseProviderPanel {
|
|||||||
render() {
|
render() {
|
||||||
const provider = this.wizard.provider as OAuth2Provider | undefined;
|
const provider = this.wizard.provider as OAuth2Provider | undefined;
|
||||||
const errors = this.wizard.errors.provider;
|
const errors = this.wizard.errors.provider;
|
||||||
|
const showClientSecretCallback = (show: boolean) => {
|
||||||
return html`<ak-wizard-title>${msg("Configure OAuth2/OpenId Provider")}</ak-wizard-title>
|
this.showClientSecret = show;
|
||||||
<form class="pf-c-form pf-m-horizontal" @input=${this.handleChange}>
|
};
|
||||||
<ak-text-input
|
return renderForm(provider ?? {}, errors, this.showClientSecret, showClientSecretCallback);
|
||||||
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-element-horizontal
|
|
||||||
name="invalidationFlow"
|
|
||||||
label=${msg("Invalidation flow")}
|
|
||||||
.errorMessages=${errors?.invalidationFlow ?? []}
|
|
||||||
?required=${true}
|
|
||||||
>
|
|
||||||
<ak-flow-search
|
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
|
||||||
.currentFlow=${provider?.invalidationFlow}
|
|
||||||
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>
|
|
||||||
|
|
||||||
<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-textarea-input
|
|
||||||
name="redirectUris"
|
|
||||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
|
||||||
.value=${provider?.redirectUris}
|
|
||||||
.errorMessages=${errors?.redirectUriHelp ?? []}
|
|
||||||
.bighelp=${redirectUriHelp}
|
|
||||||
>
|
|
||||||
</ak-textarea-input>
|
|
||||||
|
|
||||||
<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 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>
|
|
||||||
</form>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ export class ProviderWizard extends AKElement {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ak-wizard-page-type-create
|
<ak-wizard-page-type-create
|
||||||
|
name="selectProviderType"
|
||||||
slot="initial"
|
slot="initial"
|
||||||
layout=${TypeCreateWizardPageLayouts.grid}
|
layout=${TypeCreateWizardPageLayouts.grid}
|
||||||
.types=${this.providerTypes}
|
.types=${this.providerTypes}
|
||||||
|
@ -1,116 +1,11 @@
|
|||||||
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 { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
||||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
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";
|
|
||||||
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 { TemplateResult, html } from "lit";
|
|
||||||
import { customElement, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
|
||||||
|
|
||||||
import {
|
import { ClientTypeEnum, OAuth2Provider, ProvidersApi } from "@goauthentik/api";
|
||||||
ClientTypeEnum,
|
|
||||||
FlowsInstancesListDesignationEnum,
|
|
||||||
IssuerModeEnum,
|
|
||||||
OAuth2Provider,
|
|
||||||
ProvidersApi,
|
|
||||||
SubModeEnum,
|
|
||||||
} from "@goauthentik/api";
|
|
||||||
|
|
||||||
import {
|
import { renderForm } from "./OAuth2ProviderFormForm.js";
|
||||||
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 URLs 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 this 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>`,
|
|
||||||
)}`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Form page for OAuth2 Authentication Method
|
* Form page for OAuth2 Authentication Method
|
||||||
@ -145,233 +40,11 @@ export class OAuth2ProviderFormPage extends BaseProviderForm<OAuth2Provider> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(): TemplateResult {
|
renderForm() {
|
||||||
const provider = this.instance;
|
const showClientSecretCallback = (show: boolean) => {
|
||||||
|
this.showClientSecret = show;
|
||||||
return html` <ak-text-input
|
};
|
||||||
name="name"
|
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
|
||||||
label=${msg("Name")}
|
|
||||||
value=${ifDefined(provider?.name)}
|
|
||||||
required
|
|
||||||
></ak-text-input>
|
|
||||||
|
|
||||||
<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-textarea-input
|
|
||||||
name="redirectUris"
|
|
||||||
label=${msg("Redirect URIs/Origins (RegEx)")}
|
|
||||||
.value=${provider?.redirectUris}
|
|
||||||
.bighelp=${redirectUriHelp}
|
|
||||||
>
|
|
||||||
</ak-textarea-input>
|
|
||||||
|
|
||||||
<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("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
|
|
||||||
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-element-horizontal
|
|
||||||
label=${msg("Invalidation flow")}
|
|
||||||
name="invalidationFlow"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<ak-flow-search
|
|
||||||
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
|
||||||
.currentFlow=${provider?.invalidationFlow}
|
|
||||||
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>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
354
web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts
Normal file
354
web/src/admin/providers/oauth2/OAuth2ProviderFormForm.ts
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
|
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";
|
||||||
|
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,
|
||||||
|
OAuth2Provider,
|
||||||
|
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 URLs 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 this 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)}
|
||||||
|
.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-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="${first(provider?.clientId, randomString(40, ascii_letters + digits))}"
|
||||||
|
.errorMessages=${errors?.clientId ?? []}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
<ak-text-input
|
||||||
|
name="clientSecret"
|
||||||
|
label=${msg("Client Secret")}
|
||||||
|
value="${first(
|
||||||
|
provider?.clientSecret,
|
||||||
|
randomString(128, ascii_letters + digits),
|
||||||
|
)}"
|
||||||
|
?hidden=${!showClientSecret}
|
||||||
|
.errorMessages=${errors?.clientSecret ?? []}
|
||||||
|
>
|
||||||
|
</ak-text-input>
|
||||||
|
<ak-textarea-input
|
||||||
|
name="redirectUris"
|
||||||
|
label=${msg("Redirect URIs/Origins (RegEx)")}
|
||||||
|
.value=${provider?.redirectUris}
|
||||||
|
.errorMessages=${errors?.redirectUriHelp ?? []}
|
||||||
|
.bighelp=${redirectUriHelp}
|
||||||
|
>
|
||||||
|
</ak-textarea-input>
|
||||||
|
|
||||||
|
<ak-form-element-horizontal
|
||||||
|
label=${msg("Signing Key")}
|
||||||
|
name="signingKey"
|
||||||
|
.errorMessages=${errors?.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("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
|
||||||
|
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-element-horizontal
|
||||||
|
label=${msg("Invalidation flow")}
|
||||||
|
name="invalidationFlow"
|
||||||
|
.errorMessages=${errors?.invalidationFlow ?? []}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<ak-flow-search
|
||||||
|
flowType=${FlowsInstancesListDesignationEnum.Invalidation}
|
||||||
|
.currentFlow=${provider?.invalidationFlow}
|
||||||
|
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")}"
|
||||||
|
required
|
||||||
|
.errorMessages=${errors?.refreshTokenValidity ?? []}
|
||||||
|
.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>`;
|
||||||
|
}
|
@ -44,37 +44,43 @@ export class FormGroup extends AKElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render(): TemplateResult {
|
render(): TemplateResult {
|
||||||
return html`<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
|
return html` <div class="pf-c-form">
|
||||||
<div class="pf-c-form__field-group-toggle">
|
<div class="pf-c-form__field-group ${this.expanded ? "pf-m-expanded" : ""}">
|
||||||
<div class="pf-c-form__field-group-toggle-button">
|
<div class="pf-c-form__field-group-toggle">
|
||||||
<button
|
<div class="pf-c-form__field-group-toggle-button">
|
||||||
class="pf-c-button pf-m-plain"
|
<button
|
||||||
type="button"
|
class="pf-c-button pf-m-plain"
|
||||||
aria-expanded="${this.expanded}"
|
type="button"
|
||||||
aria-label=${this.ariaLabel}
|
aria-expanded="${this.expanded}"
|
||||||
@click=${() => {
|
aria-label=${this.ariaLabel}
|
||||||
this.expanded = !this.expanded;
|
@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 class="pf-c-form__field-group-toggle-icon">
|
||||||
</span>
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
</button>
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="pf-c-form__field-group-header">
|
||||||
<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-main">
|
<div class="pf-c-form__field-group-header-title">
|
||||||
<div class="pf-c-form__field-group-header-title">
|
<div class="pf-c-form__field-group-header-title-text">
|
||||||
<div class="pf-c-form__field-group-header-title-text">
|
<slot name="header"></slot>
|
||||||
<slot name="header"></slot>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pf-c-form__field-group-header-description">
|
||||||
|
<slot name="description"></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-form__field-group-header-description">
|
|
||||||
<slot name="description"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<slot
|
||||||
|
?hidden=${!this.expanded}
|
||||||
|
class="pf-c-form__field-group-body"
|
||||||
|
name="body"
|
||||||
|
></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot ?hidden=${!this.expanded} class="pf-c-form__field-group-body" name="body"></slot>
|
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
102
web/tests/pageobjects/controls.ts
Normal file
102
web/tests/pageobjects/controls.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { browser } from "@wdio/globals";
|
||||||
|
import { match } from "ts-pattern";
|
||||||
|
import { ChainablePromiseArray, Key } from "webdriverio";
|
||||||
|
|
||||||
|
browser.addCommand('findByText', async function(items: ChainablePromiseArray, text: string) {
|
||||||
|
let item: WebdriverIO.Element | undefined = undefined;
|
||||||
|
for (const i of items) {
|
||||||
|
const label = await i.getText();
|
||||||
|
if (label.indexOf(text) !== -1) {
|
||||||
|
item = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
export async function 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");
|
||||||
|
|
||||||
|
// @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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setTextInput(name: string, value: string) {
|
||||||
|
const control = await $(`input[name="${name}"]`);
|
||||||
|
await control.scrollIntoView();
|
||||||
|
await control.setValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setTypeCreate(name: string, value: string) {
|
||||||
|
const control = await $(`ak-wizard-page-type-create[name="${name}"]`);
|
||||||
|
await control.scrollIntoView();
|
||||||
|
const cards = ;
|
||||||
|
const selection = await findByText(await control.$$("div.pf-c-card__title"), value);
|
||||||
|
await selection.scrollIntoView();
|
||||||
|
await selection.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setFormGroup(name: string, setting: "open" | "closed") {
|
||||||
|
const formGroup = await $(`.//span[contains(., "${name}")]`);
|
||||||
|
await formGroup.scrollIntoView();
|
||||||
|
const toggle = await formGroup.$("div.pf-c-form__field-group-toggle-button button");
|
||||||
|
await match([toggle.getAttribute("expanded"), setting])
|
||||||
|
.with(["false", "open"], async () => await toggle.click())
|
||||||
|
.with(["true", "closed"], async () => await toggle.click())
|
||||||
|
.otherwise(async () => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clickButton(name: string, ctx?: WebdriverIO.Element) {
|
||||||
|
const context = ctx ?? browser;
|
||||||
|
const buttons = await context.$$("button");
|
||||||
|
let button: WebdriverIO.Element;
|
||||||
|
for (const b of buttons) {
|
||||||
|
const label = await b.getText();
|
||||||
|
if (label.indexOf(name) !== -1) {
|
||||||
|
button = b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await button.scrollIntoView();
|
||||||
|
await button.click();
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { browser } from "@wdio/globals";
|
import { browser } from "@wdio/globals";
|
||||||
|
import { match } from "ts-pattern";
|
||||||
import { Key } from "webdriverio";
|
import { Key } from "webdriverio";
|
||||||
|
|
||||||
const CLICK_TIME_DELAY = 250;
|
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
|
* Main page object containing all methods, selectors and functionality that is shared across all
|
||||||
* page objects
|
* page objects
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class Page {
|
export default class Page {
|
||||||
/**
|
/**
|
||||||
* Opens a sub page of the 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
|
* why it would be hard to simplify this further (`flow` vs `tentanted-flow` vs a straight-up
|
||||||
* SearchSelect each have different a `searchSelector`).
|
* SearchSelect each have different a `searchSelector`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
async searchSelect(searchSelector: string, managedSelector: string, buttonSelector: string) {
|
||||||
const inputBind = await $(searchSelector);
|
const inputBind = await $(searchSelector);
|
||||||
const inputMain = await inputBind.$('input[type="text"]');
|
const inputMain = await inputBind.$('input[type="text"]');
|
||||||
@ -55,6 +56,77 @@ export default class Page {
|
|||||||
await browser.keys(Key.Tab);
|
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");
|
||||||
|
|
||||||
|
// @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([toggle.getAttribute("expanded"), setting])
|
||||||
|
.with(["false", "open"], async () => await toggle.click())
|
||||||
|
.with(["true", "closed"], async () => await toggle.click())
|
||||||
|
.otherwise(async () => {});
|
||||||
|
}
|
||||||
|
|
||||||
public async logout() {
|
public async logout() {
|
||||||
await browser.url("http://localhost:9000/flows/-/default/invalidation/");
|
await browser.url("http://localhost:9000/flows/-/default/invalidation/");
|
||||||
return await this.pause();
|
return await this.pause();
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { expect } from "@wdio/globals";
|
import { expect } from "@wdio/globals";
|
||||||
|
import {
|
||||||
|
clickButton,
|
||||||
|
setFormGroup,
|
||||||
|
setSearchSelect,
|
||||||
|
setTextInput,
|
||||||
|
setTypeCreate,
|
||||||
|
} from "pageobjects/controls.js";
|
||||||
|
|
||||||
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
|
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
|
||||||
import ProvidersListPage from "../pageobjects/providers-list.page.js";
|
import ProvidersListPage from "../pageobjects/providers-list.page.js";
|
||||||
@ -16,6 +23,14 @@ async function reachTheProvider() {
|
|||||||
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
|
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fillOutFields(fields: FieldDesc[]) {
|
||||||
|
for (const field of fields) {
|
||||||
|
const thefunc = field[0];
|
||||||
|
const args = field.slice(1);
|
||||||
|
await thefunc.apply($, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("Configure Oauth2 Providers", () => {
|
describe("Configure Oauth2 Providers", () => {
|
||||||
it("Should configure a simple LDAP Application", async () => {
|
it("Should configure a simple LDAP Application", async () => {
|
||||||
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
|
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
|
||||||
@ -23,25 +38,19 @@ describe("Configure Oauth2 Providers", () => {
|
|||||||
await reachTheProvider();
|
await reachTheProvider();
|
||||||
|
|
||||||
await $("ak-wizard-page-type-create").waitForDisplayed();
|
await $("ak-wizard-page-type-create").waitForDisplayed();
|
||||||
await $('div[data-ouid-component-name="oauth2provider"]').scrollIntoView();
|
await setTypeCreate("selectProviderType", "OAuth2/OpenID Provider");
|
||||||
await $('div[data-ouid-component-name="oauth2provider"]').click();
|
await clickButton("Next");
|
||||||
await ProviderWizardView.nextButton.click();
|
|
||||||
|
// prettier-ignore
|
||||||
|
await fillOutFields([
|
||||||
|
[setTextInput, "name", newProviderName],
|
||||||
|
[setFormGroup, "Flow settings", "open"],
|
||||||
|
[setSearchSelect, "authenticationFlow", "default-authentication-flow"],
|
||||||
|
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
|
||||||
|
[setSearchSelect, "invalidationFlow", "default-invalidation-flow"],
|
||||||
|
]);
|
||||||
|
|
||||||
await ProviderWizardView.pause();
|
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.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);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user