Committed harmony on SAML. Streamlined the tests even further.

This commit is contained in:
Ken Sternberg
2024-10-24 09:35:31 -07:00
parent a36cc820bd
commit 99af95b10c
5 changed files with 491 additions and 797 deletions

View File

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

View File

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

View File

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

View File

@ -1,28 +1,43 @@
import { expect } from "@wdio/globals";
import {
clickButton,
setFormGroup,
setSearchSelect,
setTextInput,
setTypeCreate,
} from "pageobjects/controls.js";
import ProviderWizardView from "../pageobjects/provider-wizard.page.js";
import ProvidersListPage from "../pageobjects/providers-list.page.js";
import { randomId } from "../utils/index.js";
import { login } from "../utils/login.js";
import {
simpleLDAPProviderForm,
simpleOAuth2ProviderForm,
simpleRadiusProviderForm,
} from "./shared-sequences.js";
async function reachTheProvider() {
await ProvidersListPage.logout();
await login();
await ProvidersListPage.open();
await expect(await ProvidersListPage.pageHeader()).toHaveText("Providers");
await expect(await containedMessages()).not.toContain("Successfully created provider.");
await ProvidersListPage.startWizardButton.click();
await ProviderWizardView.wizardTitle.waitForDisplayed();
await expect(await ProviderWizardView.wizardTitle).toHaveText("New provider");
}
const containedMessages = async () =>
await (async () => {
const messages = [];
for await (const alert of $("ak-message-container").$$("ak-message")) {
messages.push(await alert.$("p.pf-c-alert__title").getText());
}
return messages;
})();
const hasProviderSuccessMessage = async () =>
await browser.waitUntil(
async () => (await containedMessages()).includes("Successfully created provider."),
{ timeout: 1000, timeoutMsg: "Expected to see provider success message." },
);
type FieldDesc = [(..._: unknown) => Promise<void>, ...unknown];
async function fillOutFields(fields: FieldDesc[]) {
for (const field of fields) {
const thefunc = field[0];
@ -33,64 +48,44 @@ async function fillOutFields(fields: FieldDesc[]) {
describe("Configure Oauth2 Providers", () => {
it("Should configure a simple OAuth2 Provider", async () => {
const newProviderName = `New OAuth2 Provider - ${randomId()}`;
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
// prettier-ignore
await fillOutFields([
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newProviderName],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
]);
await fillOutFields(simpleOAuth2ProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});
describe("Configure LDAP Providers", () => {
it("Should configure a simple LDAP Provider", async () => {
const newProviderName = `New LDAP Provider - ${randomId()}`;
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
// prettier-ignore
await fillOutFields([
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newProviderName],
[setFormGroup, /Flow settings/, "open"],
// This will never not weird me out.
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
[setSearchSelect, "invalidationFlow", "default-invalidation-flow"],
]);
await fillOutFields(simpleLDAPProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});
describe("Configure Radius Providers", () => {
it("Should configure a simple Radius Provider", async () => {
const newProviderName = `New Radius Provider - ${randomId()}`;
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
// prettier-ignore
await fillOutFields([
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newProviderName],
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
]);
await fillOutFields(simpleRadiusProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});
describe("Configure SAML Providers", () => {
it("Should configure a simple Radius Provider", async () => {
await reachTheProvider();
await $("ak-wizard-page-type-create").waitForDisplayed();
await fillOutFields(simpleRadiusProviderForm());
await ProviderWizardView.pause();
await ProviderWizardView.nextButton.click();
await hasProviderSuccessMessage();
});
});

View File

@ -0,0 +1,35 @@
import {
clickButton,
setFormGroup,
setSearchSelect,
setTextInput,
setTypeCreate,
} from "pageobjects/controls.js";
import { randomId } from "../utils/index.js";
const newObjectName = (prefix: string) => `${prefix} - ${randomId()}`;
export const simpleOAuth2ProviderForm = () => [
[setTypeCreate, "selectProviderType", "OAuth2/OpenID Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Oauth2 Provider")],
[setSearchSelect, "authorizationFlow", "default-provider-authorization-explicit-consent"],
];
export const simpleLDAPProviderForm = () => [
[setTypeCreate, "selectProviderType", "LDAP Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New LDAP Provider")],
// This will never not weird me out.
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
[setFormGroup, /Flow settings/, "open"],
[setSearchSelect, "invalidationFlow", "default-invalidation-flow"],
];
export const simpleRadiusProviderForm = () => [
[setTypeCreate, "selectProviderType", "Radius Provider"],
[clickButton, "Next"],
[setTextInput, "name", newObjectName("New Radius Provider")],
[setSearchSelect, "authorizationFlow", "default-authentication-flow"],
];