From ccdac9413dc32369ecbbc8912fe0a367c5daefa2 Mon Sep 17 00:00:00 2001 From: Stefan Jacobi Date: Mon, 26 Feb 2024 09:10:34 +0100 Subject: [PATCH] feat(webauthn): make webauthn params configurable * add attachment, attestation preference and resident key requirement params to create and update tenant endpoints * add fallback values when params are not provided * add migrations for new fields in webauthn config in database * reflect changes in README.md and admin spec Closes: #45 --- server/README.md | 16 +++++++++---- server/api/dto/admin/request/webauthn.go | 22 +++++++++++++++--- server/api/middleware/webauthn.go | 6 ++--- server/api/services/registration_service.go | 18 ++++++++++----- ...40220140353_add_webauthn_options.down.fizz | 3 +++ ...0240220140353_add_webauthn_options.up.fizz | 3 +++ server/persistence/models/webauthn_config.go | 23 +++++++++++-------- spec/passkey-server-admin.yaml | 23 +++++++++++++++++++ 8 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 server/persistence/migrations/20240220140353_add_webauthn_options.down.fizz create mode 100644 server/persistence/migrations/20240220140353_add_webauthn_options.up.fizz diff --git a/server/README.md b/server/README.md index 774c972..01e002d 100644 --- a/server/README.md +++ b/server/README.md @@ -143,7 +143,9 @@ curl --location 'http://:8001/tenants' \ ] }, "timeout": 60000, - "user_verification": "preferred" + "user_verification": "preferred", + "attestation_preference": "none", + "resident_key_requirement": "required" }, "create_api_key": true } @@ -225,7 +227,9 @@ the [WebAuthn Relying Party](https://www.w3.org/TR/webauthn-2/#webauthn-relying- ] }, "timeout": 60000, - "user_verification": "preferred" + "user_verification": "preferred", + "attestation_preference": "none", + "resident_key_requirement": "required" } } } @@ -252,7 +256,9 @@ As an example: If the login should be available at `https://login.example.com` i ] }, "timeout": 60000, - "user_verification": "preferred" + "user_verification": "preferred", + "attestation_preference": "none", + "resident_key_requirement": "required" } } } @@ -275,7 +281,9 @@ point. Then the WebAuthn config would look like this: ] }, "timeout": 60000, - "user_verification": "preferred" + "user_verification": "preferred", + "attestation_preference": "none", + "resident_key_requirement": "required" } } } diff --git a/server/api/dto/admin/request/webauthn.go b/server/api/dto/admin/request/webauthn.go index cbe22f3..7bea68c 100644 --- a/server/api/dto/admin/request/webauthn.go +++ b/server/api/dto/admin/request/webauthn.go @@ -8,9 +8,12 @@ import ( ) type CreateWebauthnDto struct { - RelyingParty CreateRelyingPartyDto `json:"relying_party" validate:"required"` - Timeout int `json:"timeout" validate:"required,number"` - UserVerification protocol.UserVerificationRequirement `json:"user_verification" validate:"required,oneof=required preferred discouraged"` + RelyingParty CreateRelyingPartyDto `json:"relying_party" validate:"required"` + Timeout int `json:"timeout" validate:"required,number"` + UserVerification protocol.UserVerificationRequirement `json:"user_verification" validate:"required,oneof=required preferred discouraged"` + Attachment *protocol.AuthenticatorAttachment `json:"attachment" validate:"omitempty,oneof=platform cross-platform"` + AttestationPreference *protocol.ConveyancePreference `json:"attestation_preference" validate:"omitempty,oneof=none indirect direct enterprise"` + ResidentKeyRequirement *protocol.ResidentKeyRequirement `json:"resident_key_requirement" validate:"omitempty,oneof=discouraged preferred required"` } func (dto *CreateWebauthnDto) ToModel(configModel models.Config) models.WebauthnConfig { @@ -24,6 +27,19 @@ func (dto *CreateWebauthnDto) ToModel(configModel models.Config) models.Webauthn CreatedAt: now, UpdatedAt: now, UserVerification: dto.UserVerification, + Attachment: dto.Attachment, + } + + if dto.AttestationPreference == nil { + webauthnConfig.AttestationPreference = protocol.PreferNoAttestation + } else { + webauthnConfig.AttestationPreference = *dto.AttestationPreference + } + + if dto.ResidentKeyRequirement == nil { + webauthnConfig.ResidentKeyRequirement = protocol.ResidentKeyRequirementRequired + } else { + webauthnConfig.ResidentKeyRequirement = *dto.ResidentKeyRequirement } return webauthnConfig diff --git a/server/api/middleware/webauthn.go b/server/api/middleware/webauthn.go index 63e562f..fd210d6 100644 --- a/server/api/middleware/webauthn.go +++ b/server/api/middleware/webauthn.go @@ -30,11 +30,11 @@ func WebauthnMiddleware() echo.MiddlewareFunc { RPDisplayName: cfg.WebauthnConfig.RelyingParty.DisplayName, RPID: cfg.WebauthnConfig.RelyingParty.RPId, RPOrigins: origins, - AttestationPreference: protocol.PreferNoAttestation, + AttestationPreference: cfg.WebauthnConfig.AttestationPreference, AuthenticatorSelection: protocol.AuthenticatorSelection{ RequireResidentKey: &f, - ResidentKey: protocol.ResidentKeyRequirementDiscouraged, - UserVerification: protocol.VerificationRequired, + ResidentKey: cfg.WebauthnConfig.ResidentKeyRequirement, + UserVerification: cfg.WebauthnConfig.UserVerification, }, Debug: false, Timeouts: webauthn.TimeoutsConfig{ diff --git a/server/api/services/registration_service.go b/server/api/services/registration_service.go index fea24bf..0328259 100644 --- a/server/api/services/registration_service.go +++ b/server/api/services/registration_service.go @@ -51,14 +51,20 @@ func (rs *registrationService) Initialize(user *models.WebauthnUser) (*protocol. } t := true + authSelection := protocol.AuthenticatorSelection{ + RequireResidentKey: &t, + ResidentKey: rs.tenant.Config.WebauthnConfig.ResidentKeyRequirement, + UserVerification: rs.tenant.Config.WebauthnConfig.UserVerification, + } + + if rs.tenant.Config.WebauthnConfig.Attachment != nil { + authSelection.AuthenticatorAttachment = *rs.tenant.Config.WebauthnConfig.Attachment + } + credentialCreation, sessionData, err := rs.webauthnClient.BeginRegistration( internalUser, - webauthn.WithAuthenticatorSelection(protocol.AuthenticatorSelection{ - RequireResidentKey: &t, - ResidentKey: protocol.ResidentKeyRequirementRequired, - UserVerification: rs.tenant.Config.WebauthnConfig.UserVerification, - }), - webauthn.WithConveyancePreference(protocol.PreferNoAttestation), + webauthn.WithAuthenticatorSelection(authSelection), + webauthn.WithConveyancePreference(rs.tenant.Config.WebauthnConfig.AttestationPreference), ) if err != nil { return nil, internalUser.UserId, err diff --git a/server/persistence/migrations/20240220140353_add_webauthn_options.down.fizz b/server/persistence/migrations/20240220140353_add_webauthn_options.down.fizz new file mode 100644 index 0000000..63eb4bb --- /dev/null +++ b/server/persistence/migrations/20240220140353_add_webauthn_options.down.fizz @@ -0,0 +1,3 @@ +drop_column("webauthn_configs", "resident_key_requirement") +drop_column("webauthn_configs", "attestation_preference") +drop_column("webauthn_configs", "attachment") diff --git a/server/persistence/migrations/20240220140353_add_webauthn_options.up.fizz b/server/persistence/migrations/20240220140353_add_webauthn_options.up.fizz new file mode 100644 index 0000000..8ccbd09 --- /dev/null +++ b/server/persistence/migrations/20240220140353_add_webauthn_options.up.fizz @@ -0,0 +1,3 @@ +add_column("webauthn_configs", "attachment", "string", { "null": true }) +add_column("webauthn_configs", "attestation_preference", "string", { default: "none" }) +add_column("webauthn_configs", "resident_key_requirement", "string", { default: "required" }) diff --git a/server/persistence/models/webauthn_config.go b/server/persistence/models/webauthn_config.go index 3a71b7b..95f0d16 100644 --- a/server/persistence/models/webauthn_config.go +++ b/server/persistence/models/webauthn_config.go @@ -12,23 +12,28 @@ import ( // WebauthnConfig is used by pop to map your webauthn_configs database table to your go code. type WebauthnConfig struct { - ID uuid.UUID `json:"id" db:"id"` - Config *Config `json:"config" belongs_to:"configs"` - ConfigID uuid.UUID `json:"config_id" db:"config_id"` - RelyingParty RelyingParty `json:"relying_party" has_one:"relying_parties"` - Timeout int `json:"timeout" db:"timeout"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - UserVerification protocol.UserVerificationRequirement `json:"user_verification" db:"user_verification"` + ID uuid.UUID `json:"id" db:"id"` + Config *Config `json:"config" belongs_to:"configs"` + ConfigID uuid.UUID `json:"config_id" db:"config_id"` + RelyingParty RelyingParty `json:"relying_party" has_one:"relying_parties"` + Timeout int `json:"timeout" db:"timeout"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + UserVerification protocol.UserVerificationRequirement `json:"user_verification" db:"user_verification"` + Attachment *protocol.AuthenticatorAttachment `json:"attachment" db:"attachment"` + AttestationPreference protocol.ConveyancePreference `json:"attestation_preference" db:"attestation_preference"` + ResidentKeyRequirement protocol.ResidentKeyRequirement `json:"resident_key_requirement" db:"resident_key_requirement"` } // Validate gets run every time you call a "pop.Validate*" (pop.ValidateAndSave, pop.ValidateAndCreate, pop.ValidateAndUpdate) method. // This method is not required and may be deleted. -func (webauthn *WebauthnConfig) Validate(tx *pop.Connection) (*validate.Errors, error) { +func (webauthn *WebauthnConfig) Validate(_ *pop.Connection) (*validate.Errors, error) { return validate.Validate( &validators.UUIDIsPresent{Name: "ID", Field: webauthn.ID}, &validators.IntIsPresent{Name: "Timeout", Field: webauthn.Timeout}, &validators.StringIsPresent{Name: "UserVerification", Field: string(webauthn.UserVerification)}, + &validators.StringIsPresent{Name: "AttestationPreference", Field: string(webauthn.AttestationPreference)}, + &validators.StringIsPresent{Name: "ResidentKeyRequirement", Field: string(webauthn.ResidentKeyRequirement)}, &validators.TimeIsPresent{Name: "UpdatedAt", Field: webauthn.UpdatedAt}, &validators.TimeIsPresent{Name: "CreatedAt", Field: webauthn.CreatedAt}, ), nil diff --git a/spec/passkey-server-admin.yaml b/spec/passkey-server-admin.yaml index b12b7fa..8cb32c5 100644 --- a/spec/passkey-server-admin.yaml +++ b/spec/passkey-server-admin.yaml @@ -580,6 +580,7 @@ components: maxLength: 36 api_key: $ref: '#/components/schemas/secret' + description: omitted when `create_api_key`is omitted or set to `false` required: - id secret: @@ -658,10 +659,32 @@ components: examples: - 60000 user_verification: + type: string enum: - required - preferred - discouraged + attachment: + type: string + enum: + - platform + - cross-platform + description: uses all authenticator attachments when omitted + attestation_preference: + type: string + enum: + - none + - indirect + - direct + - enterprise + description: defaults to `none` when omitted + resident_key_requirement: + type: string + enum: + - discouraged + - preferred + - required + description: defaults to `required` when omitted required: - relying_party - timeout