enterprise/stages: Add MTLS stage (#14296)
* prepare client auth with inbuilt server Signed-off-by: Jens Langhammer <jens@goauthentik.io> * introduce better IPC auth Signed-off-by: Jens Langhammer <jens@goauthentik.io> * init Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only allow trusted proxies to set MTLS headers Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more stage progress Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dont fail if ipc_key doesn't exist Signed-off-by: Jens Langhammer <jens@goauthentik.io> * actually install app Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add some tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update API Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix unquote Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix int serial number not jsonable Signed-off-by: Jens Langhammer <jens@goauthentik.io> * init ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix git pull in makefile Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix parse helper Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add test for outpost Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more tests and improvements Signed-off-by: Jens Langhammer <jens@goauthentik.io> * improve labels Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add support for multiple CAs on brand Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add support for multiple CAs to MTLS stage Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dont log ipcuser secret views Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix go mod Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -1,9 +1,12 @@
|
||||
import { certificateProvider, certificateSelector } from "@goauthentik/admin/brands/Certificates";
|
||||
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 { DefaultBrand } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
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 { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
@ -303,6 +306,17 @@ export class BrandForm extends ModelForm<Brand, string> {
|
||||
.certificate=${this.instance?.webCertificate}
|
||||
></ak-crypto-certificate-search>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Client Certificates")}
|
||||
name="clientCertificates"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${certificateProvider}
|
||||
.selector=${certificateSelector(this.instance?.clientCertificates)}
|
||||
available-label=${msg("Available Certificates")}
|
||||
selected-label=${msg("Selected Certificates")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.YAML}
|
||||
|
||||
39
web/src/admin/brands/Certificates.ts
Normal file
39
web/src/admin/brands/Certificates.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
|
||||
import { CertificateKeyPair, CryptoApi } from "@goauthentik/api";
|
||||
|
||||
const certToSelect = (s: CertificateKeyPair) => [s.pk, s.name, s.name, s];
|
||||
|
||||
export async function certificateProvider(page = 1, search = "") {
|
||||
const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
|
||||
ordering: "name",
|
||||
pageSize: 20,
|
||||
search: search.trim(),
|
||||
page,
|
||||
hasKey: undefined,
|
||||
});
|
||||
return {
|
||||
pagination: certificates.pagination,
|
||||
options: certificates.results.map(certToSelect),
|
||||
};
|
||||
}
|
||||
|
||||
export function certificateSelector(instanceMappings?: string[]) {
|
||||
if (!instanceMappings) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return async () => {
|
||||
const pm = new CryptoApi(DEFAULT_CONFIG);
|
||||
const mappings = await Promise.allSettled(
|
||||
instanceMappings.map((instanceId) =>
|
||||
pm.cryptoCertificatekeypairsRetrieve({ kpUuid: instanceId }),
|
||||
),
|
||||
);
|
||||
|
||||
return mappings
|
||||
.filter((s) => s.status === "fulfilled")
|
||||
.map((s) => s.value)
|
||||
.map(certToSelect);
|
||||
};
|
||||
}
|
||||
@ -16,6 +16,7 @@ import "@goauthentik/admin/stages/dummy/DummyStageForm";
|
||||
import "@goauthentik/admin/stages/email/EmailStageForm";
|
||||
import "@goauthentik/admin/stages/identification/IdentificationStageForm";
|
||||
import "@goauthentik/admin/stages/invitation/InvitationStageForm";
|
||||
import "@goauthentik/admin/stages/mtls/MTLSStageForm";
|
||||
import "@goauthentik/admin/stages/password/PasswordStageForm";
|
||||
import "@goauthentik/admin/stages/prompt/PromptStageForm";
|
||||
import "@goauthentik/admin/stages/redirect/RedirectStageForm";
|
||||
|
||||
@ -14,6 +14,7 @@ import "@goauthentik/admin/stages/dummy/DummyStageForm";
|
||||
import "@goauthentik/admin/stages/email/EmailStageForm";
|
||||
import "@goauthentik/admin/stages/identification/IdentificationStageForm";
|
||||
import "@goauthentik/admin/stages/invitation/InvitationStageForm";
|
||||
import "@goauthentik/admin/stages/mtls/MTLSStageForm";
|
||||
import "@goauthentik/admin/stages/password/PasswordStageForm";
|
||||
import "@goauthentik/admin/stages/prompt/PromptStageForm";
|
||||
import "@goauthentik/admin/stages/redirect/RedirectStageForm";
|
||||
|
||||
162
web/src/admin/stages/mtls/MTLSStageForm.ts
Normal file
162
web/src/admin/stages/mtls/MTLSStageForm.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { certificateProvider, certificateSelector } from "@goauthentik/admin/brands/Certificates";
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
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 { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
CertAttributeEnum,
|
||||
MutualTLSStage,
|
||||
MutualTLSStageModeEnum,
|
||||
StagesApi,
|
||||
UserAttributeEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-mtls-form")
|
||||
export class MTLSStageForm extends BaseStageForm<MutualTLSStage> {
|
||||
loadInstance(pk: string): Promise<MutualTLSStage> {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesMtlsRetrieve({
|
||||
stageUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
async send(data: MutualTLSStage): Promise<MutualTLSStage> {
|
||||
if (this.instance) {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesMtlsUpdate({
|
||||
stageUuid: this.instance.pk || "",
|
||||
mutualTLSStageRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesMtlsCreate({
|
||||
mutualTLSStageRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<span> ${msg("Client-certificate/mTLS authentication/enrollment.")} </span>
|
||||
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name || "")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("Stage-specific settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Certificate optional"),
|
||||
value: MutualTLSStageModeEnum.Optional,
|
||||
default: true,
|
||||
description: html`${msg(
|
||||
"If no certificate was provided, this stage will succeed and continue to the next stage.",
|
||||
)}`,
|
||||
},
|
||||
{
|
||||
label: msg("Certificate required"),
|
||||
value: MutualTLSStageModeEnum.Required,
|
||||
description: html`${msg(
|
||||
"If no certificate was provided, this stage will stop flow execution.",
|
||||
)}`,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.mode}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate authorities")}
|
||||
name="certificateAuthorities"
|
||||
>
|
||||
<ak-dual-select-dynamic-selected
|
||||
.provider=${certificateProvider}
|
||||
.selector=${certificateSelector(this.instance?.certificateAuthorities)}
|
||||
available-label=${msg("Available Certificates")}
|
||||
selected-label=${msg("Selected Certificates")}
|
||||
></ak-dual-select-dynamic-selected>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure the certificate authority client certificates are validated against. The certificate authority can also be configured on a brand, which allows for different certificate authorities for different domains.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Certificate attribute")}
|
||||
required
|
||||
name="certAttribute"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Common Name"),
|
||||
value: CertAttributeEnum.CommonName,
|
||||
},
|
||||
{
|
||||
label: msg("Email"),
|
||||
value: CertAttributeEnum.Email,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
label: msg("Subject"),
|
||||
value: CertAttributeEnum.Subject,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.certAttribute}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Configure the attribute of the certificate used to look for a user.",
|
||||
)}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("User attribute")}
|
||||
required
|
||||
name="userAttribute"
|
||||
>
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("Username"),
|
||||
value: UserAttributeEnum.Username,
|
||||
},
|
||||
{
|
||||
label: msg("Email"),
|
||||
value: UserAttributeEnum.Email,
|
||||
default: true,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.userAttribute}
|
||||
>
|
||||
</ak-radio>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Configure the attribute of the user used to look for a user.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ak-stage-mtls-form": MTLSStageForm;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user