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:
		@ -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>
 | 
			
		||||
        `;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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")}"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								web/src/admin/stages/authenticator_webauthn/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								web/src/admin/stages/authenticator_webauthn/utils.ts
									
									
									
									
									
										Normal 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,
 | 
			
		||||
    ];
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user