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 "@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 { 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 { ClientTypeEnum, FlowsInstancesListDesignationEnum, SourcesApi } from "@goauthentik/api";
|
||||
import { SourcesApi } from "@goauthentik/api";
|
||||
import { type OAuth2Provider, type PaginatedOAuthSourceList } from "@goauthentik/api";
|
||||
|
||||
import BaseProviderPanel from "../BaseProviderPanel";
|
||||
@ -59,227 +31,10 @@ 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>
|
||||
<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-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>`;
|
||||
const showClientSecretCallback = (show: boolean) => {
|
||||
this.showClientSecret = show;
|
||||
};
|
||||
return renderForm(provider ?? {}, errors, this.showClientSecret, showClientSecretCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ export class ProviderWizard extends AKElement {
|
||||
}}
|
||||
>
|
||||
<ak-wizard-page-type-create
|
||||
name="selectProviderType"
|
||||
slot="initial"
|
||||
layout=${TypeCreateWizardPageLayouts.grid}
|
||||
.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 { 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 { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
ClientTypeEnum,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
IssuerModeEnum,
|
||||
OAuth2Provider,
|
||||
ProvidersApi,
|
||||
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 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>`,
|
||||
)}`;
|
||||
import { renderForm } from "./OAuth2ProviderFormForm.js";
|
||||
|
||||
/**
|
||||
* Form page for OAuth2 Authentication Method
|
||||
@ -145,233 +40,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-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>`;
|
||||
renderForm() {
|
||||
const showClientSecretCallback = (show: boolean) => {
|
||||
this.showClientSecret = show;
|
||||
};
|
||||
return renderForm(this.instance ?? {}, [], this.showClientSecret, showClientSecretCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
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>`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user