stages/authenticator_validate: add ability to limit webauthn device types (#9180)

* stages/authenticator_validate: add ability to limit webauthn device types

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* reword

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* require enterprise attestation when a device restriction is configured as we need the aaguid

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improve error message

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L
2024-04-11 13:10:05 +02:00
committed by GitHub
parent 35448f6017
commit fd44bc2bec
14 changed files with 398 additions and 83 deletions

View File

@ -1,5 +1,9 @@
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/Alert";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
import { DataProvision } from "@goauthentik/elements/ak-dual-select/types";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
@ -71,7 +75,8 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
[DeviceClassesEnum.Sms, msg("SMS-based Authenticators")],
];
return html` <span>
return html`
<span>
${msg(
"Stage used to validate any authenticator. This stage should be used during authentication or authorization flows.",
)}
@ -119,7 +124,7 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
/>
<p class="pf-c-form__helper-text">
${msg(
"If any of the devices user of the types selected above have been used within this duration, this stage will be skipped.",
"If the user has successfully authenticated with a device in the classes listed above within this configured duration, this stage will be skipped.",
)}
</p>
<ak-utils-time-delta-help></ak-utils-time-delta-help>
@ -166,33 +171,6 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
</option>
</select>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("WebAuthn User verification")}
?required=${true}
name="webauthnUserVerification"
>
<ak-radio
.options=${[
{
label: msg("User verification must occur."),
value: UserVerificationEnum.Required,
default: true,
},
{
label: msg(
"User verification is preferred if available, but not required.",
),
value: UserVerificationEnum.Preferred,
},
{
label: msg("User verification should not occur."),
value: UserVerificationEnum.Discouraged,
},
]}
.value=${this.instance?.webauthnUserVerification}
>
</ak-radio>
</ak-form-element-horizontal>
${this.showConfigurationStages
? html`
<ak-form-element-horizontal
@ -228,6 +206,77 @@ export class AuthenticatorValidateStageForm extends BaseStageForm<AuthenticatorV
`
: html``}
</div>
</ak-form-group>`;
</ak-form-group>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("WebAuthn-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal
label=${msg("WebAuthn User verification")}
?required=${true}
name="webauthnUserVerification"
>
<ak-radio
.options=${[
{
label: msg("User verification must occur."),
value: UserVerificationEnum.Required,
default: true,
},
{
label: msg(
"User verification is preferred if available, but not required.",
),
value: UserVerificationEnum.Preferred,
},
{
label: msg("User verification should not occur."),
value: UserVerificationEnum.Discouraged,
},
]}
.value=${this.instance?.webauthnUserVerification}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("WebAuthn Device type restrictions")}
name="webauthnAllowedDeviceTypes"
>
<ak-dual-select-provider
.provider=${(page: number, search?: string): Promise<DataProvision> => {
return new StagesApi(DEFAULT_CONFIG)
.stagesAuthenticatorWebauthnDeviceTypesList({
page: page,
search: search,
})
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(deviceTypeRestrictionPair),
};
});
}}
.selected=${(this.instance?.webauthnAllowedDeviceTypesObj ?? []).map(
deviceTypeRestrictionPair,
)}
available-label="${msg("Available Device types")}"
selected-label="${msg("Selected Device types")}"
></ak-dual-select-provider>
<p class="pf-c-form__helper-text">
${msg(
"Optionally restrict which WebAuthn device types may be used. When no device types are selected, all devices are allowed.",
)}
</p>
<ak-alert ?inline=${true}>
${
/* TODO: Remove this after 2024.6..or maybe later? */
msg(
"This restriction only applies to devices created in authentik 2024.4 or later.",
)
}
</ak-alert>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}
}

View File

@ -1,9 +1,7 @@
import { RenderFlowOption } from "@goauthentik/admin/flows/utils";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import {
DataProvision,
DualSelectPair,
} from "@goauthentik/authentik/elements/ak-dual-select/types";
import { deviceTypeRestrictionPair } from "@goauthentik/admin/stages/authenticator_webauthn/utils";
import { DataProvision } from "@goauthentik/authentik/elements/ak-dual-select/types";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { first } from "@goauthentik/common/utils";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider";
@ -25,23 +23,12 @@ import {
ResidentKeyRequirementEnum,
StagesApi,
UserVerificationEnum,
WebAuthnDeviceType,
} from "@goauthentik/api";
@customElement("ak-stage-authenticator-webauthn-form")
export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorWebAuthnStage> {
deviceTypeRestrictionPair(item: WebAuthnDeviceType): DualSelectPair {
const label = item.description ? item.description : item.aaguid;
return [
item.aaguid,
html`<div class="selection-main">${label}</div>
<div class="selection-desc">${item.aaguid}</div>`,
label,
];
}
loadInstance(pk: string): Promise<AuthenticatorWebAuthnStage> {
return new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRetrieve({
async loadInstance(pk: string): Promise<AuthenticatorWebAuthnStage> {
return await new StagesApi(DEFAULT_CONFIG).stagesAuthenticatorWebauthnRetrieve({
stageUuid: pk,
});
}
@ -194,14 +181,12 @@ export class AuthenticatorWebAuthnStageForm extends BaseStageForm<AuthenticatorW
.then((results) => {
return {
pagination: results.pagination,
options: results.results.map(
this.deviceTypeRestrictionPair,
),
options: results.results.map(deviceTypeRestrictionPair),
};
});
}}
.selected=${(this.instance?.deviceTypeRestrictionsObj ?? []).map(
this.deviceTypeRestrictionPair,
deviceTypeRestrictionPair,
)}
available-label="${msg("Available Device types")}"
selected-label="${msg("Selected Device types")}"

View File

@ -0,0 +1,15 @@
import { DualSelectPair } from "@goauthentik/elements/ak-dual-select/types";
import { html } from "lit";
import { WebAuthnDeviceType } from "@goauthentik/api";
export function deviceTypeRestrictionPair(item: WebAuthnDeviceType): DualSelectPair {
const label = item.description ? item.description : item.aaguid;
return [
item.aaguid,
html`<div class="selection-main">${label}</div>
<div class="selection-desc">${item.aaguid}</div>`,
label,
];
}