refactor: webauthn naming (#5243)

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/5244/head
James Elliott 2023-04-15 02:04:42 +10:00 committed by GitHub
parent 37a49b21af
commit 2733fc040c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 395 additions and 396 deletions

View File

@ -29,9 +29,9 @@ tags:
- name: User Information - name: User Information
description: User configuration endpoints description: User configuration endpoints
{{- end }} {{- end }}
{{- if (or .TOTP .Webauthn .Duo) }} {{- if (or .TOTP .WebAuthn .Duo) }}
- name: Second Factor - name: Second Factor
description: TOTP, Webauthn and Duo endpoints description: TOTP, WebAuthn and Duo endpoints
externalDocs: externalDocs:
url: https://www.authelia.com/configuration/second-factor/introduction/ url: https://www.authelia.com/configuration/second-factor/introduction/
{{- end }} {{- end }}
@ -721,13 +721,13 @@ paths:
security: security:
- authelia_auth: [] - authelia_auth: []
{{- end }} {{- end }}
{{- if .Webauthn }} {{- if .WebAuthn }}
/api/secondfactor/webauthn/assertion: /api/secondfactor/webauthn/assertion:
get: get:
tags: tags:
- Second Factor - Second Factor
summary: Second Factor Authentication - Webauthn (Request) summary: Second Factor Authentication - WebAuthn (Request)
description: This endpoint starts the second factor authentication process with the FIDO2 Webauthn credential. description: This endpoint starts the second factor authentication process with the FIDO2 WebAuthn credential.
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
@ -742,8 +742,8 @@ paths:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Second Factor Authentication - Webauthn summary: Second Factor Authentication - WebAuthn
description: This endpoint completes the second factor authentication process with the FIDO2 Webauthn credential. description: This endpoint completes the second factor authentication process with the FIDO2 WebAuthn credential.
requestBody: requestBody:
required: true required: true
content: content:
@ -765,9 +765,9 @@ paths:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Identity Verification Webauthn Credential Creation summary: Identity Verification WebAuthn Credential Creation
description: > description: >
This endpoint performs identity verification to begin the FIDO2 Webauthn credential attestation process This endpoint performs identity verification to begin the FIDO2 WebAuthn credential attestation process
(registration). (registration).
The session generated from this endpoint must be utilised for the subsequent steps in the The session generated from this endpoint must be utilised for the subsequent steps in the
@ -785,9 +785,9 @@ paths:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Identity Verification FIDO2 Webauthn Credential Validation summary: Identity Verification FIDO2 WebAuthn Credential Validation
description: > description: >
This endpoint performs identity and token verification, upon success generates a FIDO2 Webauthn device This endpoint performs identity and token verification, upon success generates a FIDO2 WebAuthn device
attestation challenge (registration). attestation challenge (registration).
The session cookie generated from the `/api/secondfactor/webauthn/identity/start` endpoint must be utilised The session cookie generated from the `/api/secondfactor/webauthn/identity/start` endpoint must be utilised
@ -811,8 +811,8 @@ paths:
post: post:
tags: tags:
- Second Factor - Second Factor
summary: Webauthn Credential Attestation summary: WebAuthn Credential Attestation
description: This endpoint performs Webauthn credential attestation (registration). description: This endpoint performs WebAuthn credential attestation (registration).
requestBody: requestBody:
required: true required: true
content: content:
@ -1878,7 +1878,7 @@ components:
type: string type: string
example: 'otpauth://totp/{{ .Domain | default "example.com" }}:john?algorithm=SHA1&digits=6&issuer=auth.{{ .Domain | default "example.com" }}&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q' example: 'otpauth://totp/{{ .Domain | default "example.com" }}:john?algorithm=SHA1&digits=6&issuer=auth.{{ .Domain | default "example.com" }}&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q'
{{- end }} {{- end }}
{{- if .Webauthn }} {{- if .WebAuthn }}
webauthn.PublicKeyCredential: webauthn.PublicKeyCredential:
type: object type: object
properties: properties:

View File

@ -214,13 +214,13 @@ totp:
## ##
## Parameters used for WebAuthn. ## Parameters used for WebAuthn.
webauthn: webauthn:
## Disable Webauthn. ## Disable WebAuthn.
disable: false disable: false
## Adjust the interaction timeout for Webauthn dialogues. ## Adjust the interaction timeout for WebAuthn dialogues.
timeout: 60s timeout: 60s
## The display name the browser should show the user for when using Webauthn to login/register. ## The display name the browser should show the user for when using WebAuthn to login/register.
display_name: Authelia display_name: Authelia
## Conveyance preference controls if we collect the attestation statement including the AAGUID from the device. ## Conveyance preference controls if we collect the attestation statement including the AAGUID from the device.
@ -1167,7 +1167,7 @@ regulation:
## ##
## Notification Provider ## Notification Provider
## ##
## Notifications are sent to users when they require a password reset, a Webauthn registration or a TOTP registration. ## Notifications are sent to users when they require a password reset, a WebAuthn registration or a TOTP registration.
## The available providers are: filesystem, smtp. You must use only one of these providers. ## The available providers are: filesystem, smtp. You must use only one of these providers.
notifier: notifier:
## You can disable the notifier startup check by setting this to true. ## You can disable the notifier startup check by setting this to true.

View File

@ -451,7 +451,7 @@ func (ctx *CmdCtx) StorageUserWebAuthnExportRunE(cmd *cobra.Command, args []stri
} }
for page := 0; true; page++ { for page := 0; true; page++ {
if devices, err = ctx.providers.StorageProvider.LoadWebauthnDevices(ctx, limit, page); err != nil { if devices, err = ctx.providers.StorageProvider.LoadWebAuthnDevices(ctx, limit, page); err != nil {
return err return err
} }
@ -522,7 +522,7 @@ func (ctx *CmdCtx) StorageUserWebAuthnImportRunE(cmd *cobra.Command, args []stri
} }
for _, device := range export.WebAuthnDevices { for _, device := range export.WebAuthnDevices {
if err = ctx.providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil { if err = ctx.providers.StorageProvider.SaveWebAuthnDevice(ctx, device); err != nil {
return err return err
} }
} }
@ -550,10 +550,10 @@ func (ctx *CmdCtx) StorageUserWebAuthnListRunE(cmd *cobra.Command, args []string
user := args[0] user := args[0]
devices, err = ctx.providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, user) devices, err = ctx.providers.StorageProvider.LoadWebAuthnDevicesByUsername(ctx, user)
switch { switch {
case len(devices) == 0 || (err != nil && errors.Is(err, storage.ErrNoWebauthnDevice)): case len(devices) == 0 || (err != nil && errors.Is(err, storage.ErrNoWebAuthnDevice)):
return fmt.Errorf("user '%s' has no webauthn devices", user) return fmt.Errorf("user '%s' has no webauthn devices", user)
case err != nil: case err != nil:
return fmt.Errorf("can't list devices for user '%s': %w", user, err) return fmt.Errorf("can't list devices for user '%s': %w", user, err)
@ -586,7 +586,7 @@ func (ctx *CmdCtx) StorageUserWebAuthnListAllRunE(_ *cobra.Command, _ []string)
output := strings.Builder{} output := strings.Builder{}
for page := 0; true; page++ { for page := 0; true; page++ {
if devices, err = ctx.providers.StorageProvider.LoadWebauthnDevices(ctx, limit, page); err != nil { if devices, err = ctx.providers.StorageProvider.LoadWebAuthnDevices(ctx, limit, page); err != nil {
return fmt.Errorf("failed to list devices: %w", err) return fmt.Errorf("failed to list devices: %w", err)
} }
@ -629,13 +629,13 @@ func (ctx *CmdCtx) StorageUserWebAuthnDeleteRunE(cmd *cobra.Command, args []stri
} }
if byKID { if byKID {
if err = ctx.providers.StorageProvider.DeleteWebauthnDevice(ctx, kid); err != nil { if err = ctx.providers.StorageProvider.DeleteWebAuthnDevice(ctx, kid); err != nil {
return fmt.Errorf("failed to delete webauthn device with kid '%s': %w", kid, err) return fmt.Errorf("failed to delete webauthn device with kid '%s': %w", kid, err)
} }
fmt.Printf("Successfully deleted WebAuthn device with key id '%s'\n", kid) fmt.Printf("Successfully deleted WebAuthn device with key id '%s'\n", kid)
} else { } else {
err = ctx.providers.StorageProvider.DeleteWebauthnDeviceByUsername(ctx, user, description) err = ctx.providers.StorageProvider.DeleteWebAuthnDeviceByUsername(ctx, user, description)
if all { if all {
if err != nil { if err != nil {

View File

@ -214,13 +214,13 @@ totp:
## ##
## Parameters used for WebAuthn. ## Parameters used for WebAuthn.
webauthn: webauthn:
## Disable Webauthn. ## Disable WebAuthn.
disable: false disable: false
## Adjust the interaction timeout for Webauthn dialogues. ## Adjust the interaction timeout for WebAuthn dialogues.
timeout: 60s timeout: 60s
## The display name the browser should show the user for when using Webauthn to login/register. ## The display name the browser should show the user for when using WebAuthn to login/register.
display_name: Authelia display_name: Authelia
## Conveyance preference controls if we collect the attestation statement including the AAGUID from the device. ## Conveyance preference controls if we collect the attestation statement including the AAGUID from the device.
@ -1167,7 +1167,7 @@ regulation:
## ##
## Notification Provider ## Notification Provider
## ##
## Notifications are sent to users when they require a password reset, a Webauthn registration or a TOTP registration. ## Notifications are sent to users when they require a password reset, a WebAuthn registration or a TOTP registration.
## The available providers are: filesystem, smtp. You must use only one of these providers. ## The available providers are: filesystem, smtp. You must use only one of these providers.
notifier: notifier:
## You can disable the notifier startup check by setting this to true. ## You can disable the notifier startup check by setting this to true.

View File

@ -21,7 +21,7 @@ type Configuration struct {
Notifier NotifierConfiguration `koanf:"notifier"` Notifier NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"` Server ServerConfiguration `koanf:"server"`
Telemetry TelemetryConfig `koanf:"telemetry"` Telemetry TelemetryConfig `koanf:"telemetry"`
Webauthn WebauthnConfiguration `koanf:"webauthn"` WebAuthn WebAuthnConfiguration `koanf:"webauthn"`
PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"` PasswordPolicy PasswordPolicyConfiguration `koanf:"password_policy"`
PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"` PrivacyPolicy PrivacyPolicy `koanf:"privacy_policy"`
} }

View File

@ -6,8 +6,8 @@ import (
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
) )
// WebauthnConfiguration represents the webauthn config. // WebAuthnConfiguration represents the webauthn config.
type WebauthnConfiguration struct { type WebAuthnConfiguration struct {
Disable bool `koanf:"disable"` Disable bool `koanf:"disable"`
DisplayName string `koanf:"display_name"` DisplayName string `koanf:"display_name"`
@ -17,8 +17,8 @@ type WebauthnConfiguration struct {
Timeout time.Duration `koanf:"timeout"` Timeout time.Duration `koanf:"timeout"`
} }
// DefaultWebauthnConfiguration describes the default values for the WebauthnConfiguration. // DefaultWebAuthnConfiguration describes the default values for the WebAuthnConfiguration.
var DefaultWebauthnConfiguration = WebauthnConfiguration{ var DefaultWebAuthnConfiguration = WebAuthnConfiguration{
DisplayName: "Authelia", DisplayName: "Authelia",
Timeout: time.Second * 60, Timeout: time.Second * 60,

View File

@ -43,7 +43,7 @@ func ValidateConfiguration(config *schema.Configuration, validator *schema.Struc
ValidateTOTP(config, validator) ValidateTOTP(config, validator)
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
ValidateAuthenticationBackend(&config.AuthenticationBackend, validator) ValidateAuthenticationBackend(&config.AuthenticationBackend, validator)
@ -89,7 +89,7 @@ func validateDefault2FAMethod(config *schema.Configuration, validator *schema.St
enabledMethods = append(enabledMethods, "totp") enabledMethods = append(enabledMethods, "totp")
} }
if !config.Webauthn.Disable { if !config.WebAuthn.Disable {
enabledMethods = append(enabledMethods, "webauthn") enabledMethods = append(enabledMethods, "webauthn")
} }

View File

@ -188,7 +188,7 @@ func TestValidateDefault2FAMethod(t *testing.T) {
}, },
}, },
{ {
desc: "ShouldAllowConfiguredMethodWebauthn", desc: "ShouldAllowConfiguredMethodWebAuthn",
have: &schema.Configuration{ have: &schema.Configuration{
Default2FAMethod: "webauthn", Default2FAMethod: "webauthn",
DuoAPI: schema.DuoAPIConfiguration{ DuoAPI: schema.DuoAPIConfiguration{
@ -225,7 +225,7 @@ func TestValidateDefault2FAMethod(t *testing.T) {
}, },
}, },
{ {
desc: "ShouldNotAllowDisabledMethodWebauthn", desc: "ShouldNotAllowDisabledMethodWebAuthn",
have: &schema.Configuration{ have: &schema.Configuration{
Default2FAMethod: "webauthn", Default2FAMethod: "webauthn",
DuoAPI: schema.DuoAPIConfiguration{ DuoAPI: schema.DuoAPIConfiguration{
@ -233,7 +233,7 @@ func TestValidateDefault2FAMethod(t *testing.T) {
IntegrationKey: "another key", IntegrationKey: "another key",
Hostname: "none", Hostname: "none",
}, },
Webauthn: schema.WebauthnConfiguration{Disable: true}, WebAuthn: schema.WebAuthnConfiguration{Disable: true},
}, },
expectedErrs: []string{ expectedErrs: []string{
"option 'default_2fa_method' must be one of the enabled options 'totp' or 'mobile_push' but it's configured as 'webauthn'", "option 'default_2fa_method' must be one of the enabled options 'totp' or 'mobile_push' but it's configured as 'webauthn'",

View File

@ -197,10 +197,10 @@ const (
"configured to an unsafe value, it should be above 8 but it's configured to %d" "configured to an unsafe value, it should be above 8 but it's configured to %d"
) )
// Webauthn Error constants. // WebAuthn Error constants.
const ( const (
errFmtWebauthnConveyancePreference = "webauthn: option 'attestation_conveyance_preference' must be one of %s but it's configured as '%s'" errFmtWebAuthnConveyancePreference = "webauthn: option 'attestation_conveyance_preference' must be one of %s but it's configured as '%s'"
errFmtWebauthnUserVerification = "webauthn: option 'user_verification' must be one of %s but it's configured as '%s'" errFmtWebAuthnUserVerification = "webauthn: option 'user_verification' must be one of %s but it's configured as '%s'"
) )
// Access Control error constants. // Access Control error constants.
@ -378,8 +378,8 @@ var (
validThemeNames = []string{"light", "dark", "grey", auto} validThemeNames = []string{"light", "dark", "grey", auto}
validSessionSameSiteValues = []string{"none", "lax", "strict"} validSessionSameSiteValues = []string{"none", "lax", "strict"}
validLogLevels = []string{"trace", "debug", "info", "warn", "error"} validLogLevels = []string{"trace", "debug", "info", "warn", "error"}
validWebauthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)} validWebAuthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)}
validWebauthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)} validWebAuthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)}
validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"} validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"}
) )

View File

@ -7,27 +7,27 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
// ValidateWebauthn validates and update Webauthn configuration. // ValidateWebAuthn validates and update WebAuthn configuration.
func ValidateWebauthn(config *schema.Configuration, validator *schema.StructValidator) { func ValidateWebAuthn(config *schema.Configuration, validator *schema.StructValidator) {
if config.Webauthn.DisplayName == "" { if config.WebAuthn.DisplayName == "" {
config.Webauthn.DisplayName = schema.DefaultWebauthnConfiguration.DisplayName config.WebAuthn.DisplayName = schema.DefaultWebAuthnConfiguration.DisplayName
} }
if config.Webauthn.Timeout <= 0 { if config.WebAuthn.Timeout <= 0 {
config.Webauthn.Timeout = schema.DefaultWebauthnConfiguration.Timeout config.WebAuthn.Timeout = schema.DefaultWebAuthnConfiguration.Timeout
} }
switch { switch {
case config.Webauthn.ConveyancePreference == "": case config.WebAuthn.ConveyancePreference == "":
config.Webauthn.ConveyancePreference = schema.DefaultWebauthnConfiguration.ConveyancePreference config.WebAuthn.ConveyancePreference = schema.DefaultWebAuthnConfiguration.ConveyancePreference
case !utils.IsStringInSlice(string(config.Webauthn.ConveyancePreference), validWebauthnConveyancePreferences): case !utils.IsStringInSlice(string(config.WebAuthn.ConveyancePreference), validWebAuthnConveyancePreferences):
validator.Push(fmt.Errorf(errFmtWebauthnConveyancePreference, strJoinOr(validWebauthnConveyancePreferences), config.Webauthn.ConveyancePreference)) validator.Push(fmt.Errorf(errFmtWebAuthnConveyancePreference, strJoinOr(validWebAuthnConveyancePreferences), config.WebAuthn.ConveyancePreference))
} }
switch { switch {
case config.Webauthn.UserVerification == "": case config.WebAuthn.UserVerification == "":
config.Webauthn.UserVerification = schema.DefaultWebauthnConfiguration.UserVerification config.WebAuthn.UserVerification = schema.DefaultWebAuthnConfiguration.UserVerification
case !utils.IsStringInSlice(string(config.Webauthn.UserVerification), validWebauthnUserVerificationRequirement): case !utils.IsStringInSlice(string(config.WebAuthn.UserVerification), validWebAuthnUserVerificationRequirement):
validator.Push(fmt.Errorf(errFmtWebauthnUserVerification, strJoinOr(validWebauthnConveyancePreferences), config.Webauthn.UserVerification)) validator.Push(fmt.Errorf(errFmtWebAuthnUserVerification, strJoinOr(validWebAuthnConveyancePreferences), config.WebAuthn.UserVerification))
} }
} }

View File

@ -11,39 +11,39 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
func TestWebauthnShouldSetDefaultValues(t *testing.T) { func TestWebAuthnShouldSetDefaultValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{}, WebAuthn: schema.WebAuthnConfiguration{},
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultWebauthnConfiguration.DisplayName, config.Webauthn.DisplayName) assert.Equal(t, schema.DefaultWebAuthnConfiguration.DisplayName, config.WebAuthn.DisplayName)
assert.Equal(t, schema.DefaultWebauthnConfiguration.Timeout, config.Webauthn.Timeout) assert.Equal(t, schema.DefaultWebAuthnConfiguration.Timeout, config.WebAuthn.Timeout)
assert.Equal(t, schema.DefaultWebauthnConfiguration.ConveyancePreference, config.Webauthn.ConveyancePreference) assert.Equal(t, schema.DefaultWebAuthnConfiguration.ConveyancePreference, config.WebAuthn.ConveyancePreference)
assert.Equal(t, schema.DefaultWebauthnConfiguration.UserVerification, config.Webauthn.UserVerification) assert.Equal(t, schema.DefaultWebAuthnConfiguration.UserVerification, config.WebAuthn.UserVerification)
} }
func TestWebauthnShouldSetDefaultTimeoutWhenNegative(t *testing.T) { func TestWebAuthnShouldSetDefaultTimeoutWhenNegative(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Timeout: -1, Timeout: -1,
}, },
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultWebauthnConfiguration.Timeout, config.Webauthn.Timeout) assert.Equal(t, schema.DefaultWebAuthnConfiguration.Timeout, config.WebAuthn.Timeout)
} }
func TestWebauthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) { func TestWebAuthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
DisplayName: "Test", DisplayName: "Test",
Timeout: time.Second * 50, Timeout: time.Second * 50,
ConveyancePreference: protocol.PreferNoAttestation, ConveyancePreference: protocol.PreferNoAttestation,
@ -51,37 +51,37 @@ func TestWebauthnShouldNotSetDefaultValuesWhenConfigured(t *testing.T) {
}, },
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, "Test", config.Webauthn.DisplayName) assert.Equal(t, "Test", config.WebAuthn.DisplayName)
assert.Equal(t, time.Second*50, config.Webauthn.Timeout) assert.Equal(t, time.Second*50, config.WebAuthn.Timeout)
assert.Equal(t, protocol.PreferNoAttestation, config.Webauthn.ConveyancePreference) assert.Equal(t, protocol.PreferNoAttestation, config.WebAuthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationDiscouraged, config.Webauthn.UserVerification) assert.Equal(t, protocol.VerificationDiscouraged, config.WebAuthn.UserVerification)
config.Webauthn.ConveyancePreference = protocol.PreferIndirectAttestation config.WebAuthn.ConveyancePreference = protocol.PreferIndirectAttestation
config.Webauthn.UserVerification = protocol.VerificationPreferred config.WebAuthn.UserVerification = protocol.VerificationPreferred
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, protocol.PreferIndirectAttestation, config.Webauthn.ConveyancePreference) assert.Equal(t, protocol.PreferIndirectAttestation, config.WebAuthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationPreferred, config.Webauthn.UserVerification) assert.Equal(t, protocol.VerificationPreferred, config.WebAuthn.UserVerification)
config.Webauthn.ConveyancePreference = protocol.PreferDirectAttestation config.WebAuthn.ConveyancePreference = protocol.PreferDirectAttestation
config.Webauthn.UserVerification = protocol.VerificationRequired config.WebAuthn.UserVerification = protocol.VerificationRequired
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, protocol.PreferDirectAttestation, config.Webauthn.ConveyancePreference) assert.Equal(t, protocol.PreferDirectAttestation, config.WebAuthn.ConveyancePreference)
assert.Equal(t, protocol.VerificationRequired, config.Webauthn.UserVerification) assert.Equal(t, protocol.VerificationRequired, config.WebAuthn.UserVerification)
} }
func TestWebauthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) { func TestWebAuthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.Configuration{ config := &schema.Configuration{
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
DisplayName: "Test", DisplayName: "Test",
Timeout: time.Second * 50, Timeout: time.Second * 50,
ConveyancePreference: "no", ConveyancePreference: "no",
@ -89,7 +89,7 @@ func TestWebauthnShouldRaiseErrorsOnInvalidOptions(t *testing.T) {
}, },
} }
ValidateWebauthn(config, validator) ValidateWebAuthn(config, validator)
require.Len(t, validator.Errors(), 2) require.Len(t, validator.Errors(), 2)

View File

@ -8,8 +8,8 @@ const (
// ActionTOTPRegistration is the string representation of the action for which the token has been produced. // ActionTOTPRegistration is the string representation of the action for which the token has been produced.
ActionTOTPRegistration = "RegisterTOTPDevice" ActionTOTPRegistration = "RegisterTOTPDevice"
// ActionWebauthnRegistration is the string representation of the action for which the token has been produced. // ActionWebAuthnRegistration is the string representation of the action for which the token has been produced.
ActionWebauthnRegistration = "RegisterWebauthnDevice" ActionWebAuthnRegistration = "RegisterWebAuthnDevice"
// ActionResetPassword is the string representation of the action for which the token has been produced. // ActionResetPassword is the string representation of the action for which the token has been produced.
ActionResetPassword = "ResetPassword" ActionResetPassword = "ResetPassword"

View File

@ -36,7 +36,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldHaveAllConfiguredMethods
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -66,7 +66,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveTOTPFromAvailableM
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: true, Disable: true,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -88,7 +88,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveTOTPFromAvailableM
}) })
} }
func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebauthnFromAvailableMethodsWhenDisabled() { func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebAuthnFromAvailableMethodsWhenDisabled() {
s.mock.Ctx.Configuration = schema.Configuration{ s.mock.Ctx.Configuration = schema.Configuration{
DuoAPI: schema.DuoAPIConfiguration{ DuoAPI: schema.DuoAPIConfiguration{
Disable: false, Disable: false,
@ -96,7 +96,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveWebauthnFromAvaila
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: true, Disable: true,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -126,7 +126,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveDuoFromAvailableMe
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -156,7 +156,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveAllMethodsWhenNoTw
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: false, Disable: false,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: false, Disable: false,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{
@ -186,7 +186,7 @@ func (s *SecondFactorAvailableMethodsFixture) TestShouldRemoveAllMethodsWhenAllD
TOTP: schema.TOTPConfiguration{ TOTP: schema.TOTPConfiguration{
Disable: true, Disable: true,
}, },
Webauthn: schema.WebauthnConfiguration{ WebAuthn: schema.WebAuthnConfiguration{
Disable: true, Disable: true,
}, },
AccessControl: schema.AccessControlConfiguration{ AccessControl: schema.AccessControlConfiguration{

View File

@ -18,19 +18,19 @@ var WebauthnIdentityStart = middlewares.IdentityVerificationStart(middlewares.Id
MailTitle: "Register your key", MailTitle: "Register your key",
MailButtonContent: "Register", MailButtonContent: "Register",
TargetEndpoint: "/webauthn/register", TargetEndpoint: "/webauthn/register",
ActionClaim: ActionWebauthnRegistration, ActionClaim: ActionWebAuthnRegistration,
IdentityRetrieverFunc: identityRetrieverFromSession, IdentityRetrieverFunc: identityRetrieverFromSession,
}, nil) }, nil)
// WebauthnIdentityFinish the handler for finishing the identity validation. // WebauthnIdentityFinish the handler for finishing the identity validation.
var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish( var WebauthnIdentityFinish = middlewares.IdentityVerificationFinish(
middlewares.IdentityVerificationFinishArgs{ middlewares.IdentityVerificationFinishArgs{
ActionClaim: ActionWebauthnRegistration, ActionClaim: ActionWebAuthnRegistration,
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration, IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
}, SecondFactorWebauthnAttestationGET) }, SecondFactorWebAuthnAttestationGET)
// SecondFactorWebauthnAttestationGET returns the attestation challenge from the server. // SecondFactorWebAuthnAttestationGET returns the attestation challenge from the server.
func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) { func SecondFactorWebAuthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string) {
var ( var (
w *webauthn.WebAuthn w *webauthn.WebAuthn
user *model.WebAuthnUser user *model.WebAuthnUser
@ -39,15 +39,15 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
) )
if userSession, err = ctx.GetSession(); err != nil { if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation challenge", regulation.AuthTypeWebauthn) ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation challenge", regulation.AuthTypeWebAuthn)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -55,7 +55,7 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
} }
if user, err = getWebAuthnUser(ctx, userSession); err != nil { if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -64,8 +64,8 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
var credentialCreation *protocol.CredentialCreation var credentialCreation *protocol.CredentialCreation
if credentialCreation, userSession.Webauthn, err = w.BeginRegistration(user); err != nil { if credentialCreation, userSession.WebAuthn, err = w.BeginRegistration(user); err != nil {
ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s attestation challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -73,7 +73,7 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
} }
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "attestation challenge", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -81,7 +81,7 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
} }
if err = ctx.SetJSONBody(credentialCreation); err != nil { if err = ctx.SetJSONBody(credentialCreation); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -89,8 +89,8 @@ func SecondFactorWebauthnAttestationGET(ctx *middlewares.AutheliaCtx, _ string)
} }
} }
// WebauthnAttestationPOST processes the attestation challenge response from the client. // WebAuthnAttestationPOST processes the attestation challenge response from the client.
func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) { func WebAuthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
var ( var (
err error err error
w *webauthn.WebAuthn w *webauthn.WebAuthn
@ -103,23 +103,23 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
) )
if userSession, err = ctx.GetSession(); err != nil { if userSession, err = ctx.GetSession(); err != nil {
ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation response", regulation.AuthTypeWebauthn) ctx.Logger.WithError(err).Errorf("Error occurred retrieving session for %s attestation response", regulation.AuthTypeWebAuthn)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
return return
} }
if userSession.Webauthn == nil { if userSession.WebAuthn == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle attestation for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username) ctx.Logger.Errorf("WebAuthn session data is not present in order to handle attestation for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageUnableToRegisterSecurityKey) respondUnauthorized(ctx, messageUnableToRegisterSecurityKey)
@ -127,7 +127,7 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
} }
if attestationResponse, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil { if attestationResponse, err = protocol.ParseCredentialCreationResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -135,15 +135,15 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
} }
if user, err = getWebAuthnUser(ctx, userSession); err != nil { if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if credential, err = w.CreateCredential(user, *userSession.Webauthn, attestationResponse); err != nil { if credential, err = w.CreateCredential(user, *userSession.WebAuthn, attestationResponse); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -152,21 +152,21 @@ func WebauthnAttestationPOST(ctx *middlewares.AutheliaCtx) {
device := model.NewWebAuthnDeviceFromCredential(w.Config.RPID, userSession.Username, "Primary", credential) device := model.NewWebAuthnDeviceFromCredential(w.Config.RPID, userSession.Username, "Primary", credential)
if err = ctx.Providers.StorageProvider.SaveWebauthnDevice(ctx, device); err != nil { if err = ctx.Providers.StorageProvider.SaveWebAuthnDevice(ctx, device); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
userSession.Webauthn = nil userSession.WebAuthn = nil
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the attestation challenge", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the attestation challenge", regulation.AuthTypeWebAuthn, userSession.Username, err)
} }
ctx.ReplyOK() ctx.ReplyOK()
ctx.SetStatusCode(fasthttp.StatusCreated) ctx.SetStatusCode(fasthttp.StatusCreated)
ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "Webauthn Credential", "Device Name": "Primary"}) ctxLogEvent(ctx, userSession.Username, "Second Factor Method Added", map[string]any{"Action": "Second Factor Method Added", "Category": "WebAuthn Credential", "Device Name": "Primary"})
} }

View File

@ -12,8 +12,8 @@ import (
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
) )
// WebauthnAssertionGET handler starts the assertion ceremony. // WebAuthnAssertionGET handler starts the assertion ceremony.
func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) { func WebAuthnAssertionGET(ctx *middlewares.AutheliaCtx) {
var ( var (
w *webauthn.WebAuthn w *webauthn.WebAuthn
user *model.WebAuthnUser user *model.WebAuthnUser
@ -29,8 +29,8 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -38,7 +38,7 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
} }
if user, err = getWebAuthnUser(ctx, userSession); err != nil { if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -61,8 +61,8 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
var assertion *protocol.CredentialAssertion var assertion *protocol.CredentialAssertion
if assertion, userSession.Webauthn, err = w.BeginLogin(user, opts...); err != nil { if assertion, userSession.WebAuthn, err = w.BeginLogin(user, opts...); err != nil {
ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to create %s assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -70,7 +70,7 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
} }
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "assertion challenge", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -78,7 +78,7 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
} }
if err = ctx.SetJSONBody(assertion); err != nil { if err = ctx.SetJSONBody(assertion); err != nil {
ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrWriteResponseBody, regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -86,21 +86,21 @@ func WebauthnAssertionGET(ctx *middlewares.AutheliaCtx) {
} }
} }
// WebauthnAssertionPOST handler completes the assertion ceremony after verifying the challenge. // WebAuthnAssertionPOST handler completes the assertion ceremony after verifying the challenge.
// //
//nolint:gocyclo //nolint:gocyclo
func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) { func WebAuthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
var ( var (
userSession session.UserSession userSession session.UserSession
err error err error
w *webauthn.WebAuthn w *webauthn.WebAuthn
bodyJSON bodySignWebauthnRequest bodyJSON bodySignWebAuthnRequest
) )
if err = ctx.ParseBody(&bodyJSON); err != nil { if err = ctx.ParseBody(&bodyJSON); err != nil {
ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebauthn, err) ctx.Logger.Errorf(logFmtErrParseRequestBody, regulation.AuthTypeWebAuthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -115,16 +115,16 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if userSession.Webauthn == nil { if userSession.WebAuthn == nil {
ctx.Logger.Errorf("Webauthn session data is not present in order to handle assertion for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username) ctx.Logger.Errorf("WebAuthn session data is not present in order to handle assertion for user '%s'. This could indicate a user trying to POST to the wrong endpoint, or the session data is not present for the browser they used.", userSession.Username)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if w, err = newWebauthn(ctx); err != nil { if w, err = newWebAuthn(ctx); err != nil {
ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to configure %s during assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -138,7 +138,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
) )
if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil { if assertionResponse, err = protocol.ParseCredentialRequestResponseBody(bytes.NewReader(ctx.PostBody())); err != nil {
ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to parse %s assertionfor user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -146,15 +146,15 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
} }
if user, err = getWebAuthnUser(ctx, userSession); err != nil { if user, err = getWebAuthnUser(ctx, userSession); err != nil {
ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to load %s devices for assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if credential, err = w.ValidateLogin(user, *userSession.Webauthn, assertionResponse); err != nil { if credential, err = w.ValidateLogin(user, *userSession.WebAuthn, assertionResponse); err != nil {
_ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebauthn, err) _ = markAuthenticationAttempt(ctx, false, nil, userSession.Username, regulation.AuthTypeWebAuthn, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -169,8 +169,8 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
found = true found = true
if err = ctx.Providers.StorageProvider.UpdateWebauthnDeviceSignIn(ctx, device.ID, device.RPID, device.LastUsedAt, device.SignCount, device.CloneWarning); err != nil { if err = ctx.Providers.StorageProvider.UpdateWebAuthnDeviceSignIn(ctx, device.ID, device.RPID, device.LastUsedAt, device.SignCount, device.CloneWarning); err != nil {
ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s': %+v", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s': %+v", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -182,7 +182,7 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
} }
if !found { if !found {
ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebauthn, userSession.Username, credential.ID, credential.Authenticator.SignCount) ctx.Logger.Errorf("Unable to save %s device signin count for assertion challenge for user '%s' device '%x' count '%d': unable to find device", regulation.AuthTypeWebAuthn, userSession.Username, credential.ID, credential.Authenticator.SignCount)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
@ -190,25 +190,25 @@ func WebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
} }
if err = ctx.RegenerateSession(); err != nil { if err = ctx.RegenerateSession(); err != nil {
ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionRegenerate, regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebauthn, nil); err != nil { if err = markAuthenticationAttempt(ctx, true, nil, userSession.Username, regulation.AuthTypeWebAuthn, nil); err != nil {
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)
return return
} }
userSession.SetTwoFactorWebauthn(ctx.Clock.Now(), userSession.SetTwoFactorWebAuthn(ctx.Clock.Now(),
assertionResponse.Response.AuthenticatorData.Flags.UserPresent(), assertionResponse.Response.AuthenticatorData.Flags.UserPresent(),
assertionResponse.Response.AuthenticatorData.Flags.UserVerified()) assertionResponse.Response.AuthenticatorData.Flags.UserVerified())
if err = ctx.SaveSession(userSession); err != nil { if err = ctx.SaveSession(userSession); err != nil {
ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebauthn, userSession.Username, err) ctx.Logger.Errorf(logFmtErrSessionSave, "removal of the assertion challenge and authentication time", regulation.AuthTypeWebAuthn, userSession.Username, err)
respondUnauthorized(ctx, messageMFAValidationFailed) respondUnauthorized(ctx, messageMFAValidationFailed)

View File

@ -30,8 +30,8 @@ type bodySignTOTPRequest struct {
WorkflowID string `json:"workflowID"` WorkflowID string `json:"workflowID"`
} }
// bodySignWebauthnRequest is the model of the request body of WebAuthn 2FA authentication endpoint. // bodySignWebAuthnRequest is the model of the request body of WebAuthn 2FA authentication endpoint.
type bodySignWebauthnRequest struct { type bodySignWebAuthnRequest struct {
TargetURL string `json:"targetURL"` TargetURL string `json:"targetURL"`
Workflow string `json:"workflow"` Workflow string `json:"workflow"`
WorkflowID string `json:"workflowID"` WorkflowID string `json:"workflowID"`

View File

@ -22,14 +22,14 @@ func getWebAuthnUser(ctx *middlewares.AutheliaCtx, userSession session.UserSessi
user.DisplayName = user.Username user.DisplayName = user.Username
} }
if user.Devices, err = ctx.Providers.StorageProvider.LoadWebauthnDevicesByUsername(ctx, userSession.Username); err != nil { if user.Devices, err = ctx.Providers.StorageProvider.LoadWebAuthnDevicesByUsername(ctx, userSession.Username); err != nil {
return nil, err return nil, err
} }
return user, nil return user, nil
} }
func newWebauthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error) { func newWebAuthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error) {
var ( var (
u *url.URL u *url.URL
) )
@ -42,22 +42,22 @@ func newWebauthn(ctx *middlewares.AutheliaCtx) (w *webauthn.WebAuthn, err error)
origin := fmt.Sprintf("%s://%s", u.Scheme, u.Host) origin := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
config := &webauthn.Config{ config := &webauthn.Config{
RPDisplayName: ctx.Configuration.Webauthn.DisplayName, RPDisplayName: ctx.Configuration.WebAuthn.DisplayName,
RPID: rpID, RPID: rpID,
RPOrigin: origin, RPOrigin: origin,
RPIcon: "", RPIcon: "",
AttestationPreference: ctx.Configuration.Webauthn.ConveyancePreference, AttestationPreference: ctx.Configuration.WebAuthn.ConveyancePreference,
AuthenticatorSelection: protocol.AuthenticatorSelection{ AuthenticatorSelection: protocol.AuthenticatorSelection{
AuthenticatorAttachment: protocol.CrossPlatform, AuthenticatorAttachment: protocol.CrossPlatform,
UserVerification: ctx.Configuration.Webauthn.UserVerification, UserVerification: ctx.Configuration.WebAuthn.UserVerification,
RequireResidentKey: protocol.ResidentKeyNotRequired(), RequireResidentKey: protocol.ResidentKeyNotRequired(),
}, },
Timeout: int(ctx.Configuration.Webauthn.Timeout.Milliseconds()), Timeout: int(ctx.Configuration.WebAuthn.Timeout.Milliseconds()),
} }
ctx.Logger.Tracef("Creating new Webauthn RP instance with ID %s and Origins %s", config.RPID, config.RPOrigin) ctx.Logger.Tracef("Creating new WebAuthn RP instance with ID %s and Origins %s", config.RPID, config.RPOrigin)
return webauthn.New(config) return webauthn.New(config)
} }

View File

@ -13,7 +13,7 @@ import (
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
) )
func TestWebauthnGetUser(t *testing.T) { func TestWebAuthnGetUser(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{ userSession := session.UserSession{
@ -21,7 +21,7 @@ func TestWebauthnGetUser(t *testing.T) {
DisplayName: "John Smith", DisplayName: "John Smith",
} }
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{ ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{
{ {
ID: 1, ID: 1,
RPID: "https://example.com", RPID: "https://example.com",
@ -99,14 +99,14 @@ func TestWebauthnGetUser(t *testing.T) {
assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1]) assert.Equal(t, protocol.AuthenticatorTransport("nfc"), descriptors[1].Transport[1])
} }
func TestWebauthnGetUserWithoutDisplayName(t *testing.T) { func TestWebAuthnGetUserWithoutDisplayName(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{ userSession := session.UserSession{
Username: "john", Username: "john",
} }
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{ ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "john").Return([]model.WebAuthnDevice{
{ {
ID: 1, ID: 1,
RPID: "https://example.com", RPID: "https://example.com",
@ -129,14 +129,14 @@ func TestWebauthnGetUserWithoutDisplayName(t *testing.T) {
assert.Equal(t, "john", user.DisplayName) assert.Equal(t, "john", user.DisplayName)
} }
func TestWebauthnGetUserWithErr(t *testing.T) { func TestWebAuthnGetUserWithErr(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
userSession := session.UserSession{ userSession := session.UserSession{
Username: "john", Username: "john",
} }
ctx.StorageMock.EXPECT().LoadWebauthnDevicesByUsername(ctx.Ctx, "john").Return(nil, errors.New("not found")) ctx.StorageMock.EXPECT().LoadWebAuthnDevicesByUsername(ctx.Ctx, "john").Return(nil, errors.New("not found"))
user, err := getWebAuthnUser(ctx.Ctx, userSession) user, err := getWebAuthnUser(ctx.Ctx, userSession)
@ -144,24 +144,24 @@ func TestWebauthnGetUserWithErr(t *testing.T) {
assert.Nil(t, user) assert.Nil(t, user)
} }
func TestWebauthnNewWebauthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) { func TestWebAuthnNewWebAuthnShouldReturnErrWhenHeadersNotAvailable(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
ctx.Ctx.Request.Header.Del("X-Forwarded-Host") ctx.Ctx.Request.Header.Del("X-Forwarded-Host")
w, err := newWebauthn(ctx.Ctx) w, err := newWebAuthn(ctx.Ctx)
assert.Nil(t, w) assert.Nil(t, w)
assert.EqualError(t, err, "missing required X-Forwarded-Host header") assert.EqualError(t, err, "missing required X-Forwarded-Host header")
} }
func TestWebauthnNewWebauthnShouldReturnErrWhenWebauthnNotConfigured(t *testing.T) { func TestWebAuthnNewWebAuthnShouldReturnErrWhenWebAuthnNotConfigured(t *testing.T) {
ctx := mocks.NewMockAutheliaCtx(t) ctx := mocks.NewMockAutheliaCtx(t)
ctx.Ctx.Request.Header.Set("X-Forwarded-Host", "example.com") ctx.Ctx.Request.Header.Set("X-Forwarded-Host", "example.com")
ctx.Ctx.Request.Header.Set("X-Forwarded-URI", "/") ctx.Ctx.Request.Header.Set("X-Forwarded-URI", "/")
ctx.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") ctx.Ctx.Request.Header.Set("X-Forwarded-Proto", "https")
w, err := newWebauthn(ctx.Ctx) w, err := newWebAuthn(ctx.Ctx)
assert.Nil(t, w) assert.Nil(t, w)
assert.EqualError(t, err, "Configuration error: Missing RPDisplayName") assert.EqualError(t, err, "Configuration error: Missing RPDisplayName")

View File

@ -49,7 +49,7 @@ func (ctx *AutheliaCtx) AvailableSecondFactorMethods() (methods []string) {
methods = append(methods, model.SecondFactorMethodTOTP) methods = append(methods, model.SecondFactorMethodTOTP)
} }
if !ctx.Configuration.Webauthn.Disable { if !ctx.Configuration.WebAuthn.Disable {
methods = append(methods, model.SecondFactorMethodWebAuthn) methods = append(methods, model.SecondFactorMethodWebAuthn)
} }

View File

@ -245,7 +245,7 @@ func TestShouldReturnCorrectSecondFactorMethods(t *testing.T) {
assert.Equal(t, []string{model.SecondFactorMethodWebAuthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods()) assert.Equal(t, []string{model.SecondFactorMethodWebAuthn, model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())
mock.Ctx.Configuration.Webauthn.Disable = true mock.Ctx.Configuration.WebAuthn.Disable = true
assert.Equal(t, []string{model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods()) assert.Equal(t, []string{model.SecondFactorMethodDuo}, mock.Ctx.AvailableSecondFactorMethods())

View File

@ -39,7 +39,6 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
return m.recorder return m.recorder
} }
// AppendAuthenticationLog mocks base method. // AppendAuthenticationLog mocks base method.
func (m *MockStorage) AppendAuthenticationLog(arg0 context.Context, arg1 model.AuthenticationAttempt) error { func (m *MockStorage) AppendAuthenticationLog(arg0 context.Context, arg1 model.AuthenticationAttempt) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -167,32 +166,32 @@ func (mr *MockStorageMockRecorder) DeleteTOTPConfiguration(arg0, arg1 interface{
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).DeleteTOTPConfiguration), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).DeleteTOTPConfiguration), arg0, arg1)
} }
// DeleteWebauthnDevice mocks base method. // DeleteWebAuthnDevice mocks base method.
func (m *MockStorage) DeleteWebauthnDevice(arg0 context.Context, arg1 string) error { func (m *MockStorage) DeleteWebAuthnDevice(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebauthnDevice", arg0, arg1) ret := m.ctrl.Call(m, "DeleteWebAuthnDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteWebauthnDevice indicates an expected call of DeleteWebauthnDevice. // DeleteWebAuthnDevice indicates an expected call of DeleteWebAuthnDevice.
func (mr *MockStorageMockRecorder) DeleteWebauthnDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) DeleteWebAuthnDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDevice", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebAuthnDevice", reflect.TypeOf((*MockStorage)(nil).DeleteWebAuthnDevice), arg0, arg1)
} }
// DeleteWebauthnDeviceByUsername mocks base method. // DeleteWebAuthnDeviceByUsername mocks base method.
func (m *MockStorage) DeleteWebauthnDeviceByUsername(arg0 context.Context, arg1, arg2 string) error { func (m *MockStorage) DeleteWebAuthnDeviceByUsername(arg0 context.Context, arg1, arg2 string) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWebauthnDeviceByUsername", arg0, arg1, arg2) ret := m.ctrl.Call(m, "DeleteWebAuthnDeviceByUsername", arg0, arg1, arg2)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// DeleteWebauthnDeviceByUsername indicates an expected call of DeleteWebauthnDeviceByUsername. // DeleteWebAuthnDeviceByUsername indicates an expected call of DeleteWebAuthnDeviceByUsername.
func (mr *MockStorageMockRecorder) DeleteWebauthnDeviceByUsername(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) DeleteWebAuthnDeviceByUsername(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDeviceByUsername", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDeviceByUsername), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebAuthnDeviceByUsername", reflect.TypeOf((*MockStorage)(nil).DeleteWebAuthnDeviceByUsername), arg0, arg1, arg2)
} }
// FindIdentityVerification mocks base method. // FindIdentityVerification mocks base method.
@ -420,34 +419,34 @@ func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifiers(arg0 interface{}) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifiers", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifiers), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifiers", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifiers), arg0)
} }
// LoadWebauthnDevices mocks base method. // LoadWebAuthnDevices mocks base method.
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebAuthnDevice, error) { func (m *MockStorage) LoadWebAuthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebAuthnDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDevices", arg0, arg1, arg2) ret := m.ctrl.Call(m, "LoadWebAuthnDevices", arg0, arg1, arg2)
ret0, _ := ret[0].([]model.WebAuthnDevice) ret0, _ := ret[0].([]model.WebAuthnDevice)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// LoadWebauthnDevices indicates an expected call of LoadWebauthnDevices. // LoadWebAuthnDevices indicates an expected call of LoadWebAuthnDevices.
func (mr *MockStorageMockRecorder) LoadWebauthnDevices(arg0, arg1, arg2 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadWebAuthnDevices(arg0, arg1, arg2 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevices", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevices), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebAuthnDevices", reflect.TypeOf((*MockStorage)(nil).LoadWebAuthnDevices), arg0, arg1, arg2)
} }
// LoadWebauthnDevicesByUsername mocks base method. // LoadWebAuthnDevicesByUsername mocks base method.
func (m *MockStorage) LoadWebauthnDevicesByUsername(arg0 context.Context, arg1 string) ([]model.WebAuthnDevice, error) { func (m *MockStorage) LoadWebAuthnDevicesByUsername(arg0 context.Context, arg1 string) ([]model.WebAuthnDevice, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadWebauthnDevicesByUsername", arg0, arg1) ret := m.ctrl.Call(m, "LoadWebAuthnDevicesByUsername", arg0, arg1)
ret0, _ := ret[0].([]model.WebAuthnDevice) ret0, _ := ret[0].([]model.WebAuthnDevice)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// LoadWebauthnDevicesByUsername indicates an expected call of LoadWebauthnDevicesByUsername. // LoadWebAuthnDevicesByUsername indicates an expected call of LoadWebAuthnDevicesByUsername.
func (mr *MockStorageMockRecorder) LoadWebauthnDevicesByUsername(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) LoadWebAuthnDevicesByUsername(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevicesByUsername), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebAuthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebAuthnDevicesByUsername), arg0, arg1)
} }
// RevokeOAuth2PARContext mocks base method. // RevokeOAuth2PARContext mocks base method.
@ -689,18 +688,18 @@ func (mr *MockStorageMockRecorder) SaveUserOpaqueIdentifier(arg0, arg1 interface
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserOpaqueIdentifier", reflect.TypeOf((*MockStorage)(nil).SaveUserOpaqueIdentifier), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserOpaqueIdentifier", reflect.TypeOf((*MockStorage)(nil).SaveUserOpaqueIdentifier), arg0, arg1)
} }
// SaveWebauthnDevice mocks base method. // SaveWebAuthnDevice mocks base method.
func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 model.WebAuthnDevice) error { func (m *MockStorage) SaveWebAuthnDevice(arg0 context.Context, arg1 model.WebAuthnDevice) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveWebauthnDevice", arg0, arg1) ret := m.ctrl.Call(m, "SaveWebAuthnDevice", arg0, arg1)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// SaveWebauthnDevice indicates an expected call of SaveWebauthnDevice. // SaveWebAuthnDevice indicates an expected call of SaveWebAuthnDevice.
func (mr *MockStorageMockRecorder) SaveWebauthnDevice(arg0, arg1 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) SaveWebAuthnDevice(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebauthnDevice", reflect.TypeOf((*MockStorage)(nil).SaveWebauthnDevice), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveWebAuthnDevice", reflect.TypeOf((*MockStorage)(nil).SaveWebAuthnDevice), arg0, arg1)
} }
// SchemaEncryptionChangeKey mocks base method. // SchemaEncryptionChangeKey mocks base method.
@ -864,16 +863,16 @@ func (mr *MockStorageMockRecorder) UpdateTOTPConfigurationSignIn(arg0, arg1, arg
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateTOTPConfigurationSignIn), arg0, arg1, arg2) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTOTPConfigurationSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateTOTPConfigurationSignIn), arg0, arg1, arg2)
} }
// UpdateWebauthnDeviceSignIn mocks base method. // UpdateWebAuthnDeviceSignIn mocks base method.
func (m *MockStorage) UpdateWebauthnDeviceSignIn(arg0 context.Context, arg1 int, arg2 string, arg3 sql.NullTime, arg4 uint32, arg5 bool) error { func (m *MockStorage) UpdateWebAuthnDeviceSignIn(arg0 context.Context, arg1 int, arg2 string, arg3 sql.NullTime, arg4 uint32, arg5 bool) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateWebauthnDeviceSignIn", arg0, arg1, arg2, arg3, arg4, arg5) ret := m.ctrl.Call(m, "UpdateWebAuthnDeviceSignIn", arg0, arg1, arg2, arg3, arg4, arg5)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// UpdateWebauthnDeviceSignIn indicates an expected call of UpdateWebauthnDeviceSignIn. // UpdateWebAuthnDeviceSignIn indicates an expected call of UpdateWebAuthnDeviceSignIn.
func (mr *MockStorageMockRecorder) UpdateWebauthnDeviceSignIn(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { func (mr *MockStorageMockRecorder) UpdateWebAuthnDeviceSignIn(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebauthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebauthnDeviceSignIn), arg0, arg1, arg2, arg3, arg4, arg5) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWebAuthnDeviceSignIn", reflect.TypeOf((*MockStorage)(nil).UpdateWebAuthnDeviceSignIn), arg0, arg1, arg2, arg3, arg4, arg5)
} }

View File

@ -5,9 +5,9 @@ type AuthenticationMethodsReferences struct {
UsernameAndPassword bool UsernameAndPassword bool
TOTP bool TOTP bool
Duo bool Duo bool
Webauthn bool WebAuthn bool
WebauthnUserPresence bool WebAuthnUserPresence bool
WebauthnUserVerified bool WebAuthnUserVerified bool
} }
// FactorKnowledge returns true if a "something you know" factor of authentication was used. // FactorKnowledge returns true if a "something you know" factor of authentication was used.
@ -17,7 +17,7 @@ func (r AuthenticationMethodsReferences) FactorKnowledge() bool {
// FactorPossession returns true if a "something you have" factor of authentication was used. // FactorPossession returns true if a "something you have" factor of authentication was used.
func (r AuthenticationMethodsReferences) FactorPossession() bool { func (r AuthenticationMethodsReferences) FactorPossession() bool {
return r.TOTP || r.Webauthn || r.Duo return r.TOTP || r.WebAuthn || r.Duo
} }
// MultiFactorAuthentication returns true if multiple factors were used. // MultiFactorAuthentication returns true if multiple factors were used.
@ -27,7 +27,7 @@ func (r AuthenticationMethodsReferences) MultiFactorAuthentication() bool {
// ChannelBrowser returns true if a browser was used to authenticate. // ChannelBrowser returns true if a browser was used to authenticate.
func (r AuthenticationMethodsReferences) ChannelBrowser() bool { func (r AuthenticationMethodsReferences) ChannelBrowser() bool {
return r.UsernameAndPassword || r.TOTP || r.Webauthn return r.UsernameAndPassword || r.TOTP || r.WebAuthn
} }
// ChannelService returns true if a non-browser service was used to authenticate. // ChannelService returns true if a non-browser service was used to authenticate.
@ -57,15 +57,15 @@ func (r AuthenticationMethodsReferences) MarshalRFC8176() []string {
amr = append(amr, AMRShortMessageService) amr = append(amr, AMRShortMessageService)
} }
if r.Webauthn { if r.WebAuthn {
amr = append(amr, AMRHardwareSecuredKey) amr = append(amr, AMRHardwareSecuredKey)
} }
if r.WebauthnUserPresence { if r.WebAuthnUserPresence {
amr = append(amr, AMRUserPresence) amr = append(amr, AMRUserPresence)
} }
if r.WebauthnUserVerified { if r.WebAuthnUserVerified {
amr = append(amr, AMRPersonalIdentificationNumber) amr = append(amr, AMRPersonalIdentificationNumber)
} }

View File

@ -49,9 +49,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn", desc: "WebAuthn",
is: AuthenticationMethodsReferences{Webauthn: true}, is: AuthenticationMethodsReferences{WebAuthn: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: true, FactorPossession: true,
@ -63,9 +63,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn User Presence", desc: "WebAuthn User Presence",
is: AuthenticationMethodsReferences{WebauthnUserPresence: true}, is: AuthenticationMethodsReferences{WebAuthnUserPresence: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: false, FactorPossession: false,
@ -77,9 +77,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn User Verified", desc: "WebAuthn User Verified",
is: AuthenticationMethodsReferences{WebauthnUserVerified: true}, is: AuthenticationMethodsReferences{WebAuthnUserVerified: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: false, FactorPossession: false,
@ -91,9 +91,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Webauthn with User Presence and Verified", desc: "WebAuthn with User Presence and Verified",
is: AuthenticationMethodsReferences{Webauthn: true, WebauthnUserVerified: true, WebauthnUserPresence: true}, is: AuthenticationMethodsReferences{WebAuthn: true, WebAuthnUserVerified: true, WebAuthnUserPresence: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: true, FactorPossession: true,
@ -119,9 +119,9 @@ func TestAuthenticationMethodsReferences(t *testing.T) {
}, },
}, },
{ {
desc: "Duo Webauthn TOTP", desc: "Duo WebAuthn TOTP",
is: AuthenticationMethodsReferences{Duo: true, Webauthn: true, TOTP: true}, is: AuthenticationMethodsReferences{Duo: true, WebAuthn: true, TOTP: true},
want: testAMRWant{ want: testAMRWant{
FactorKnowledge: false, FactorKnowledge: false,
FactorPossession: true, FactorPossession: true,

View File

@ -195,7 +195,7 @@ const (
// a user presence test. Evidence that the end user is present and interacting with the device. This is sometimes // a user presence test. Evidence that the end user is present and interacting with the device. This is sometimes
// also referred to as "test of user presence" as per W3C.WD-webauthn-20170216. // also referred to as "test of user presence" as per W3C.WD-webauthn-20170216.
// //
// Authelia utilizes this when a user has used Webauthn to authenticate and the user presence flag was set. // Authelia utilizes this when a user has used WebAuthn to authenticate and the user presence flag was set.
// Factor: Meta, Channel: Meta. // Factor: Meta, Channel: Meta.
// //
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176 // RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
@ -208,7 +208,7 @@ const (
// containing only numbers) that a user enters to unlock a key on the device. This mechanism should have a way to // containing only numbers) that a user enters to unlock a key on the device. This mechanism should have a way to
// deter an attacker from obtaining the PIN by trying repeated guesses. // deter an attacker from obtaining the PIN by trying repeated guesses.
// //
// Authelia utilizes this when a user has used Webauthn to authenticate and the user verified flag was set. // Authelia utilizes this when a user has used WebAuthn to authenticate and the user verified flag was set.
// Factor: Meta, Channel: Meta. // Factor: Meta, Channel: Meta.
// //
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176 // RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
@ -244,7 +244,7 @@ const (
// AMRHardwareSecuredKey is an RFC8176 Authentication Method Reference Value that // AMRHardwareSecuredKey is an RFC8176 Authentication Method Reference Value that
// represents authentication via a proof-of-Possession (PoP) of a hardware-secured key. // represents authentication via a proof-of-Possession (PoP) of a hardware-secured key.
// //
// Authelia utilizes this when a user has used Webauthn to authenticate. Factor: Have, Channel: Browser. // Authelia utilizes this when a user has used WebAuthn to authenticate. Factor: Have, Channel: Browser.
// //
// RFC8176: https://datatracker.ietf.org/doc/html/rfc8176 // RFC8176: https://datatracker.ietf.org/doc/html/rfc8176
AMRHardwareSecuredKey = "hwk" AMRHardwareSecuredKey = "hwk"

View File

@ -12,8 +12,8 @@ const (
// AuthTypeTOTP is the string representing an auth log for second-factor authentication via TOTP. // AuthTypeTOTP is the string representing an auth log for second-factor authentication via TOTP.
AuthTypeTOTP = "TOTP" AuthTypeTOTP = "TOTP"
// AuthTypeWebauthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn. // AuthTypeWebAuthn is the string representing an auth log for second-factor authentication via FIDO2/CTAP2/WebAuthn.
AuthTypeWebauthn = "Webauthn" AuthTypeWebAuthn = "WebAuthn"
// AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO. // AuthTypeDuo is the string representing an auth log for second-factor authentication via DUO.
AuthTypeDuo = "Duo" AuthTypeDuo = "Duo"

View File

@ -255,14 +255,14 @@ func handleRouter(config *schema.Configuration, providers middlewares.Providers)
r.POST("/api/secondfactor/totp", middleware1FA(handlers.TimeBasedOneTimePasswordPOST)) r.POST("/api/secondfactor/totp", middleware1FA(handlers.TimeBasedOneTimePasswordPOST))
} }
if !config.Webauthn.Disable { if !config.WebAuthn.Disable {
// Webauthn Endpoints. // WebAuthn Endpoints.
r.POST("/api/secondfactor/webauthn/identity/start", middleware1FA(handlers.WebauthnIdentityStart)) r.POST("/api/secondfactor/webauthn/identity/start", middleware1FA(handlers.WebauthnIdentityStart))
r.POST("/api/secondfactor/webauthn/identity/finish", middleware1FA(handlers.WebauthnIdentityFinish)) r.POST("/api/secondfactor/webauthn/identity/finish", middleware1FA(handlers.WebauthnIdentityFinish))
r.POST("/api/secondfactor/webauthn/attestation", middleware1FA(handlers.WebauthnAttestationPOST)) r.POST("/api/secondfactor/webauthn/attestation", middleware1FA(handlers.WebAuthnAttestationPOST))
r.GET("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionGET)) r.GET("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebAuthnAssertionGET))
r.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebauthnAssertionPOST)) r.POST("/api/secondfactor/webauthn/assertion", middleware1FA(handlers.WebAuthnAssertionPOST))
} }
// Configure DUO api endpoint only if configuration exists. // Configure DUO api endpoint only if configuration exists.

View File

@ -272,7 +272,7 @@ func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileO
Theme: config.Theme, Theme: config.Theme,
EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""), EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""),
EndpointsWebauthn: !config.Webauthn.Disable, EndpointsWebAuthn: !config.WebAuthn.Disable,
EndpointsTOTP: !config.TOTP.Disable, EndpointsTOTP: !config.TOTP.Disable,
EndpointsDuo: !config.DuoAPI.Disable, EndpointsDuo: !config.DuoAPI.Disable,
EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil), EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil),
@ -304,7 +304,7 @@ type TemplatedFileOptions struct {
Theme string Theme string
EndpointsPasswordReset bool EndpointsPasswordReset bool
EndpointsWebauthn bool EndpointsWebAuthn bool
EndpointsTOTP bool EndpointsTOTP bool
EndpointsDuo bool EndpointsDuo bool
EndpointsOpenIDConnect bool EndpointsOpenIDConnect bool
@ -362,7 +362,7 @@ func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, domain, nonce st
Session: options.Session, Session: options.Session,
PasswordReset: options.EndpointsPasswordReset, PasswordReset: options.EndpointsPasswordReset,
Webauthn: options.EndpointsWebauthn, WebAuthn: options.EndpointsWebAuthn,
TOTP: options.EndpointsTOTP, TOTP: options.EndpointsTOTP,
Duo: options.EndpointsDuo, Duo: options.EndpointsDuo,
OpenIDConnect: options.EndpointsOpenIDConnect, OpenIDConnect: options.EndpointsOpenIDConnect,
@ -395,7 +395,7 @@ type TemplatedFileOpenAPIData struct {
CSPNonce string CSPNonce string
Session string Session string
PasswordReset bool PasswordReset bool
Webauthn bool WebAuthn bool
TOTP bool TOTP bool
Duo bool Duo bool
OpenIDConnect bool OpenIDConnect bool

View File

@ -179,7 +179,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true}, AuthenticationMethodRefs: oidc.AuthenticationMethodsReferences{UsernameAndPassword: true},
}, session) }, session)
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebAuthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -187,7 +187,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
session, err = provider.GetSession(ctx) session, err = provider.GetSession(ctx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, session.AuthenticationMethodRefs) assert.Equal(t, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true}, session.AuthenticationMethodRefs)
assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication()) assert.True(t, session.AuthenticationMethodRefs.MultiFactorAuthentication())
authAt, err = session.AuthenticatedTime(authorization.OneFactor) authAt, err = session.AuthenticatedTime(authorization.OneFactor)
@ -202,7 +202,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.EqualError(t, err, "invalid authorization level") assert.EqualError(t, err, "invalid authorization level")
assert.Equal(t, timeZeroFactor, authAt) assert.Equal(t, timeZeroFactor, authAt)
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebAuthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -211,10 +211,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, false, false) session.SetTwoFactorWebAuthn(timeTwoFactor, false, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -223,10 +223,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, true, false) session.SetTwoFactorWebAuthn(timeTwoFactor, true, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -235,10 +235,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserPresence: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, true, false) session.SetTwoFactorWebAuthn(timeTwoFactor, true, false)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -247,10 +247,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserPresence: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserPresence: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, false, true) session.SetTwoFactorWebAuthn(timeTwoFactor, false, true)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -259,10 +259,10 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorWebauthn(timeTwoFactor, false, true) session.SetTwoFactorWebAuthn(timeTwoFactor, false, true)
err = provider.SaveSession(ctx, session) err = provider.SaveSession(ctx, session)
assert.NoError(t, err) assert.NoError(t, err)
@ -271,7 +271,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorTOTP(timeTwoFactor) session.SetTwoFactorTOTP(timeTwoFactor)
@ -283,7 +283,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
session.SetTwoFactorTOTP(timeTwoFactor) session.SetTwoFactorTOTP(timeTwoFactor)
@ -295,7 +295,7 @@ func TestShouldSetSessionAuthenticationLevelsAMR(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, assert.Equal(t,
oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, Webauthn: true, WebauthnUserVerified: true}, oidc.AuthenticationMethodsReferences{UsernameAndPassword: true, TOTP: true, WebAuthn: true, WebAuthnUserVerified: true},
session.AuthenticationMethodRefs) session.AuthenticationMethodRefs)
} }

View File

@ -35,8 +35,8 @@ type UserSession struct {
AuthenticationMethodRefs oidc.AuthenticationMethodsReferences AuthenticationMethodRefs oidc.AuthenticationMethodsReferences
// Webauthn holds the session registration data for this session. // WebAuthn holds the session registration data for this session.
Webauthn *webauthn.SessionData WebAuthn *webauthn.SessionData
// This boolean is set to true after identity verification and checked // This boolean is set to true after identity verification and checked
// while doing the query actually updating the password. // while doing the query actually updating the password.

View File

@ -56,13 +56,13 @@ func (s *UserSession) SetTwoFactorDuo(now time.Time) {
s.AuthenticationMethodRefs.Duo = true s.AuthenticationMethodRefs.Duo = true
} }
// SetTwoFactorWebauthn sets the relevant Webauthn AMR's and sets the factor to 2FA. // SetTwoFactorWebAuthn sets the relevant WebAuthn AMR's and sets the factor to 2FA.
func (s *UserSession) SetTwoFactorWebauthn(now time.Time, userPresence, userVerified bool) { func (s *UserSession) SetTwoFactorWebAuthn(now time.Time, userPresence, userVerified bool) {
s.setTwoFactor(now) s.setTwoFactor(now)
s.AuthenticationMethodRefs.Webauthn = true s.AuthenticationMethodRefs.WebAuthn = true
s.AuthenticationMethodRefs.WebauthnUserPresence, s.AuthenticationMethodRefs.WebauthnUserVerified = userPresence, userVerified s.AuthenticationMethodRefs.WebAuthnUserPresence, s.AuthenticationMethodRefs.WebAuthnUserVerified = userPresence, userVerified
s.Webauthn = nil s.WebAuthn = nil
} }
// AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level. // AuthenticatedTime returns the unix timestamp this session authenticated successfully at the given level.

View File

@ -11,7 +11,7 @@ const (
tableTOTPConfigurations = "totp_configurations" tableTOTPConfigurations = "totp_configurations"
tableUserOpaqueIdentifier = "user_opaque_identifier" tableUserOpaqueIdentifier = "user_opaque_identifier"
tableUserPreferences = "user_preferences" tableUserPreferences = "user_preferences"
tableWebauthnDevices = "webauthn_devices" tableWebAuthnDevices = "webauthn_devices"
tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti" tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti"
tableOAuth2ConsentSession = "oauth2_consent_session" tableOAuth2ConsentSession = "oauth2_consent_session"

View File

@ -11,8 +11,8 @@ var (
// ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB. // ErrNoTOTPConfiguration error thrown when no TOTP configuration has been found in DB.
ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user") ErrNoTOTPConfiguration = errors.New("no TOTP configuration for user")
// ErrNoWebauthnDevice error thrown when no Webauthn device handle has been found in DB. // ErrNoWebAuthnDevice error thrown when no WebAuthn device handle has been found in DB.
ErrNoWebauthnDevice = errors.New("no Webauthn device found") ErrNoWebAuthnDevice = errors.New("no WebAuthn device found")
// ErrNoDuoDevice error thrown when no Duo device and method has been found in DB. // ErrNoDuoDevice error thrown when no Duo device and method has been found in DB.
ErrNoDuoDevice = errors.New("no Duo device and method saved") ErrNoDuoDevice = errors.New("no Duo device and method saved")

View File

@ -38,12 +38,12 @@ type Provider interface {
LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error) LoadTOTPConfiguration(ctx context.Context, username string) (config *model.TOTPConfiguration, err error)
LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error) LoadTOTPConfigurations(ctx context.Context, limit, page int) (configs []model.TOTPConfiguration, err error)
SaveWebauthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error) SaveWebAuthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error)
UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error) UpdateWebAuthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error)
DeleteWebauthnDevice(ctx context.Context, kid string) (err error) DeleteWebAuthnDevice(ctx context.Context, kid string) (err error)
DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error) DeleteWebAuthnDeviceByUsername(ctx context.Context, username, description string) (err error)
LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error) LoadWebAuthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error)
LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebAuthnDevice, err error) LoadWebAuthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebAuthnDevice, err error)
SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error) SavePreferredDuoDevice(ctx context.Context, device model.DuoDevice) (err error)
DeletePreferredDuoDevice(ctx context.Context, username string) (err error) DeletePreferredDuoDevice(ctx context.Context, username string) (err error)

View File

@ -46,16 +46,16 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations), sqlUpdateTOTPConfigRecordSignIn: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignIn, tableTOTPConfigurations),
sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations), sqlUpdateTOTPConfigRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateTOTPConfigRecordSignInByUsername, tableTOTPConfigurations),
sqlUpsertWebauthnDevice: fmt.Sprintf(queryFmtUpsertWebauthnDevice, tableWebauthnDevices), sqlUpsertWebAuthnDevice: fmt.Sprintf(queryFmtUpsertWebAuthnDevice, tableWebAuthnDevices),
sqlSelectWebauthnDevices: fmt.Sprintf(queryFmtSelectWebauthnDevices, tableWebauthnDevices), sqlSelectWebAuthnDevices: fmt.Sprintf(queryFmtSelectWebAuthnDevices, tableWebAuthnDevices),
sqlSelectWebauthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebauthnDevicesByUsername, tableWebauthnDevices), sqlSelectWebAuthnDevicesByUsername: fmt.Sprintf(queryFmtSelectWebAuthnDevicesByUsername, tableWebAuthnDevices),
sqlUpdateWebauthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices), sqlUpdateWebAuthnDeviceRecordSignIn: fmt.Sprintf(queryFmtUpdateWebAuthnDeviceRecordSignIn, tableWebAuthnDevices),
sqlUpdateWebauthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignInByUsername, tableWebauthnDevices), sqlUpdateWebAuthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebAuthnDeviceRecordSignInByUsername, tableWebAuthnDevices),
sqlDeleteWebauthnDevice: fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices), sqlDeleteWebAuthnDevice: fmt.Sprintf(queryFmtDeleteWebAuthnDevice, tableWebAuthnDevices),
sqlDeleteWebauthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices), sqlDeleteWebAuthnDeviceByUsername: fmt.Sprintf(queryFmtDeleteWebAuthnDeviceByUsername, tableWebAuthnDevices),
sqlDeleteWebauthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices), sqlDeleteWebAuthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebAuthnDeviceByUsernameAndDescription, tableWebAuthnDevices),
sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices), sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),
sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices), sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),
@ -63,7 +63,7 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
sqlUpsertPreferred2FAMethod: fmt.Sprintf(queryFmtUpsertPreferred2FAMethod, tableUserPreferences), sqlUpsertPreferred2FAMethod: fmt.Sprintf(queryFmtUpsertPreferred2FAMethod, tableUserPreferences),
sqlSelectPreferred2FAMethod: fmt.Sprintf(queryFmtSelectPreferred2FAMethod, tableUserPreferences), sqlSelectPreferred2FAMethod: fmt.Sprintf(queryFmtSelectPreferred2FAMethod, tableUserPreferences),
sqlSelectUserInfo: fmt.Sprintf(queryFmtSelectUserInfo, tableTOTPConfigurations, tableWebauthnDevices, tableDuoDevices, tableUserPreferences), sqlSelectUserInfo: fmt.Sprintf(queryFmtSelectUserInfo, tableTOTPConfigurations, tableWebAuthnDevices, tableDuoDevices, tableUserPreferences),
sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier), sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier),
sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier), sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier),
@ -165,16 +165,16 @@ type SQLProvider struct {
sqlUpdateTOTPConfigRecordSignInByUsername string sqlUpdateTOTPConfigRecordSignInByUsername string
// Table: webauthn_devices. // Table: webauthn_devices.
sqlUpsertWebauthnDevice string sqlUpsertWebAuthnDevice string
sqlSelectWebauthnDevices string sqlSelectWebAuthnDevices string
sqlSelectWebauthnDevicesByUsername string sqlSelectWebAuthnDevicesByUsername string
sqlUpdateWebauthnDeviceRecordSignIn string sqlUpdateWebAuthnDeviceRecordSignIn string
sqlUpdateWebauthnDeviceRecordSignInByUsername string sqlUpdateWebAuthnDeviceRecordSignInByUsername string
sqlDeleteWebauthnDevice string sqlDeleteWebAuthnDevice string
sqlDeleteWebauthnDeviceByUsername string sqlDeleteWebAuthnDeviceByUsername string
sqlDeleteWebauthnDeviceByUsernameAndDescription string sqlDeleteWebAuthnDeviceByUsernameAndDescription string
// Table: duo_devices. // Table: duo_devices.
sqlUpsertDuoDevice string sqlUpsertDuoDevice string
@ -823,7 +823,7 @@ func (p *SQLProvider) SaveTOTPConfiguration(ctx context.Context, config model.TO
return nil return nil
} }
// UpdateTOTPConfigurationSignIn updates a registered Webauthn devices sign in information. // UpdateTOTPConfigurationSignIn updates a registered WebAuthn devices sign in information.
func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt sql.NullTime) (err error) { func (p *SQLProvider) UpdateTOTPConfigurationSignIn(ctx context.Context, id int, lastUsedAt sql.NullTime) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigRecordSignIn, lastUsedAt, id); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlUpdateTOTPConfigRecordSignIn, lastUsedAt, id); err != nil {
return fmt.Errorf("error updating TOTP configuration id %d: %w", id, err) return fmt.Errorf("error updating TOTP configuration id %d: %w", id, err)
@ -881,95 +881,95 @@ func (p *SQLProvider) LoadTOTPConfigurations(ctx context.Context, limit, page in
return configs, nil return configs, nil
} }
// SaveWebauthnDevice saves a registered Webauthn device. // SaveWebAuthnDevice saves a registered WebAuthn device.
func (p *SQLProvider) SaveWebauthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error) { func (p *SQLProvider) SaveWebAuthnDevice(ctx context.Context, device model.WebAuthnDevice) (err error) {
if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil { if device.PublicKey, err = p.encrypt(device.PublicKey); err != nil {
return fmt.Errorf("error encrypting Webauthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err) return fmt.Errorf("error encrypting WebAuthn device public key for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
if _, err = p.db.ExecContext(ctx, p.sqlUpsertWebauthnDevice, if _, err = p.db.ExecContext(ctx, p.sqlUpsertWebAuthnDevice,
device.CreatedAt, device.LastUsedAt, device.CreatedAt, device.LastUsedAt,
device.RPID, device.Username, device.Description, device.RPID, device.Username, device.Description,
device.KID, device.PublicKey, device.KID, device.PublicKey,
device.AttestationType, device.Transport, device.AAGUID, device.SignCount, device.CloneWarning, device.AttestationType, device.Transport, device.AAGUID, device.SignCount, device.CloneWarning,
); err != nil { ); err != nil {
return fmt.Errorf("error upserting Webauthn device for user '%s' kid '%x': %w", device.Username, device.KID, err) return fmt.Errorf("error upserting WebAuthn device for user '%s' kid '%x': %w", device.Username, device.KID, err)
} }
return nil return nil
} }
// UpdateWebauthnDeviceSignIn updates a registered Webauthn devices sign in information. // UpdateWebAuthnDeviceSignIn updates a registered WebAuthn devices sign in information.
func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error) { func (p *SQLProvider) UpdateWebAuthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt sql.NullTime, signCount uint32, cloneWarning bool) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebauthnDeviceRecordSignIn, rpid, lastUsedAt, signCount, cloneWarning, id); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlUpdateWebAuthnDeviceRecordSignIn, rpid, lastUsedAt, signCount, cloneWarning, id); err != nil {
return fmt.Errorf("error updating Webauthn signin metadata for id '%x': %w", id, err) return fmt.Errorf("error updating WebAuthn signin metadata for id '%x': %w", id, err)
} }
return nil return nil
} }
// DeleteWebauthnDevice deletes a registered Webauthn device. // DeleteWebAuthnDevice deletes a registered WebAuthn device.
func (p *SQLProvider) DeleteWebauthnDevice(ctx context.Context, kid string) (err error) { func (p *SQLProvider) DeleteWebAuthnDevice(ctx context.Context, kid string) (err error) {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDevice, kid); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebAuthnDevice, kid); err != nil {
return fmt.Errorf("error deleting webauthn device with kid '%s': %w", kid, err) return fmt.Errorf("error deleting WebAuthn device with kid '%s': %w", kid, err)
} }
return nil return nil
} }
// DeleteWebauthnDeviceByUsername deletes registered Webauthn devices by username or username and description. // DeleteWebAuthnDeviceByUsername deletes registered WebAuthn devices by username or username and description.
func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error) { func (p *SQLProvider) DeleteWebAuthnDeviceByUsername(ctx context.Context, username, description string) (err error) {
if len(username) == 0 { if len(username) == 0 {
return fmt.Errorf("error deleting webauthn device with username '%s' and description '%s': username must not be empty", username, description) return fmt.Errorf("error deleting WebAuthn device with username '%s' and description '%s': username must not be empty", username, description)
} }
if len(description) == 0 { if len(description) == 0 {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsername, username); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebAuthnDeviceByUsername, username); err != nil {
return fmt.Errorf("error deleting webauthn devices for username '%s': %w", username, err) return fmt.Errorf("error deleting WebAuthn devices for username '%s': %w", username, err)
} }
} else { } else {
if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsernameAndDescription, username, description); err != nil { if _, err = p.db.ExecContext(ctx, p.sqlDeleteWebAuthnDeviceByUsernameAndDescription, username, description); err != nil {
return fmt.Errorf("error deleting webauthn device with username '%s' and description '%s': %w", username, description, err) return fmt.Errorf("error deleting WebAuthn device with username '%s' and description '%s': %w", username, description, err)
} }
} }
return nil return nil
} }
// LoadWebauthnDevices loads Webauthn device registrations. // LoadWebAuthnDevices loads WebAuthn device registrations.
func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error) { func (p *SQLProvider) LoadWebAuthnDevices(ctx context.Context, limit, page int) (devices []model.WebAuthnDevice, err error) {
devices = make([]model.WebAuthnDevice, 0, limit) devices = make([]model.WebAuthnDevice, 0, limit)
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevices, limit, limit*page); err != nil { if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebAuthnDevices, limit, limit*page); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
return nil, fmt.Errorf("error selecting Webauthn devices: %w", err) return nil, fmt.Errorf("error selecting WebAuthn devices: %w", err)
} }
for i, device := range devices { for i, device := range devices {
if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil { if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil {
return nil, fmt.Errorf("error decrypting Webauthn public key for user '%s': %w", device.Username, err) return nil, fmt.Errorf("error decrypting WebAuthn public key for user '%s': %w", device.Username, err)
} }
} }
return devices, nil return devices, nil
} }
// LoadWebauthnDevicesByUsername loads all webauthn devices registration for a given username. // LoadWebAuthnDevicesByUsername loads all WebAuthn devices registration for a given username.
func (p *SQLProvider) LoadWebauthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebAuthnDevice, err error) { func (p *SQLProvider) LoadWebAuthnDevicesByUsername(ctx context.Context, username string) (devices []model.WebAuthnDevice, err error) {
if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebauthnDevicesByUsername, username); err != nil { if err = p.db.SelectContext(ctx, &devices, p.sqlSelectWebAuthnDevicesByUsername, username); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNoWebauthnDevice return nil, ErrNoWebAuthnDevice
} }
return nil, fmt.Errorf("error selecting Webauthn devices for user '%s': %w", username, err) return nil, fmt.Errorf("error selecting WebAuthn devices for user '%s': %w", username, err)
} }
for i, device := range devices { for i, device := range devices {
if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil { if devices[i].PublicKey, err = p.decrypt(device.PublicKey); err != nil {
return nil, fmt.Errorf("error decrypting Webauthn public key for user '%s': %w", username, err) return nil, fmt.Errorf("error decrypting WebAuthn public key for user '%s': %w", username, err)
} }
} }

View File

@ -31,7 +31,7 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
// Specific alterations to this provider. // Specific alterations to this provider.
// PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead. // PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead.
provider.sqlUpsertWebauthnDevice = fmt.Sprintf(queryFmtUpsertWebauthnDevicePostgreSQL, tableWebauthnDevices) provider.sqlUpsertWebAuthnDevice = fmt.Sprintf(queryFmtUpsertWebAuthnDevicePostgreSQL, tableWebAuthnDevices)
provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtUpsertDuoDevicePostgreSQL, tableDuoDevices) provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtUpsertDuoDevicePostgreSQL, tableDuoDevices)
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtUpsertTOTPConfigurationPostgreSQL, tableTOTPConfigurations) provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtUpsertTOTPConfigurationPostgreSQL, tableTOTPConfigurations)
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtUpsertPreferred2FAMethodPostgreSQL, tableUserPreferences) provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtUpsertPreferred2FAMethodPostgreSQL, tableUserPreferences)
@ -59,13 +59,13 @@ func NewPostgreSQLProvider(config *schema.Configuration, caCertPool *x509.CertPo
provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig) provider.sqlDeleteTOTPConfig = provider.db.Rebind(provider.sqlDeleteTOTPConfig)
provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs) provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices) provider.sqlSelectWebAuthnDevices = provider.db.Rebind(provider.sqlSelectWebAuthnDevices)
provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername) provider.sqlSelectWebAuthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebAuthnDevicesByUsername)
provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn) provider.sqlUpdateWebAuthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebAuthnDeviceRecordSignIn)
provider.sqlUpdateWebauthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignInByUsername) provider.sqlUpdateWebAuthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebAuthnDeviceRecordSignInByUsername)
provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice) provider.sqlDeleteWebAuthnDevice = provider.db.Rebind(provider.sqlDeleteWebAuthnDevice)
provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername) provider.sqlDeleteWebAuthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebAuthnDeviceByUsername)
provider.sqlDeleteWebauthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDescription) provider.sqlDeleteWebAuthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebAuthnDeviceByUsernameAndDescription)
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice) provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)
provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice) provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)

View File

@ -34,7 +34,7 @@ func (p *SQLProvider) SchemaEncryptionChangeKey(ctx context.Context, key string)
encChangeFuncs := []EncryptionChangeKeyFunc{ encChangeFuncs := []EncryptionChangeKeyFunc{
schemaEncryptionChangeKeyTOTP, schemaEncryptionChangeKeyTOTP,
schemaEncryptionChangeKeyWebauthn, schemaEncryptionChangeKeyWebAuthn,
} }
for i := 0; true; i++ { for i := 0; true; i++ {
@ -90,7 +90,7 @@ func (p *SQLProvider) SchemaEncryptionCheckKey(ctx context.Context, verbose bool
if verbose { if verbose {
encCheckFuncs := []EncryptionCheckKeyFunc{ encCheckFuncs := []EncryptionCheckKeyFunc{
schemaEncryptionCheckKeyTOTP, schemaEncryptionCheckKeyTOTP,
schemaEncryptionCheckKeyWebauthn, schemaEncryptionCheckKeyWebAuthn,
} }
for i := 0; true; i++ { for i := 0; true; i++ {
@ -153,10 +153,10 @@ func schemaEncryptionChangeKeyTOTP(ctx context.Context, provider *SQLProvider, t
return nil return nil
} }
func schemaEncryptionChangeKeyWebauthn(ctx context.Context, provider *SQLProvider, tx *sqlx.Tx, key [32]byte) (err error) { func schemaEncryptionChangeKeyWebAuthn(ctx context.Context, provider *SQLProvider, tx *sqlx.Tx, key [32]byte) (err error) {
var count int var count int
if err = tx.GetContext(ctx, &count, fmt.Sprintf(queryFmtSelectRowCount, tableWebauthnDevices)); err != nil { if err = tx.GetContext(ctx, &count, fmt.Sprintf(queryFmtSelectRowCount, tableWebAuthnDevices)); err != nil {
return err return err
} }
@ -164,29 +164,29 @@ func schemaEncryptionChangeKeyWebauthn(ctx context.Context, provider *SQLProvide
return nil return nil
} }
devices := make([]encWebauthnDevice, 0, count) devices := make([]encWebAuthnDevice, 0, count)
if err = tx.SelectContext(ctx, &devices, fmt.Sprintf(queryFmtSelectWebauthnDevicesEncryptedData, tableWebauthnDevices)); err != nil { if err = tx.SelectContext(ctx, &devices, fmt.Sprintf(queryFmtSelectWebAuthnDevicesEncryptedData, tableWebAuthnDevices)); err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil return nil
} }
return fmt.Errorf("error selecting Webauthn devices: %w", err) return fmt.Errorf("error selecting WebAuthn devices: %w", err)
} }
query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebauthnDevicePublicKey, tableWebauthnDevices)) query := provider.db.Rebind(fmt.Sprintf(queryFmtUpdateWebAuthnDevicePublicKey, tableWebAuthnDevices))
for _, d := range devices { for _, d := range devices {
if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil { if d.PublicKey, err = provider.decrypt(d.PublicKey); err != nil {
return fmt.Errorf("error decrypting Webauthn device public key with id '%d': %w", d.ID, err) return fmt.Errorf("error decrypting WebAuthn device public key with id '%d': %w", d.ID, err)
} }
if d.PublicKey, err = utils.Encrypt(d.PublicKey, &key); err != nil { if d.PublicKey, err = utils.Encrypt(d.PublicKey, &key); err != nil {
return fmt.Errorf("error encrypting Webauthn device public key with id '%d': %w", d.ID, err) return fmt.Errorf("error encrypting WebAuthn device public key with id '%d': %w", d.ID, err)
} }
if _, err = tx.ExecContext(ctx, query, d.PublicKey, d.ID); err != nil { if _, err = tx.ExecContext(ctx, query, d.PublicKey, d.ID); err != nil {
return fmt.Errorf("error updating Webauthn device public key with id '%d': %w", d.ID, err) return fmt.Errorf("error updating WebAuthn device public key with id '%d': %w", d.ID, err)
} }
} }
@ -262,17 +262,17 @@ func schemaEncryptionCheckKeyTOTP(ctx context.Context, provider *SQLProvider) (t
return tableTOTPConfigurations, result return tableTOTPConfigurations, result
} }
func schemaEncryptionCheckKeyWebauthn(ctx context.Context, provider *SQLProvider) (table string, result EncryptionValidationTableResult) { func schemaEncryptionCheckKeyWebAuthn(ctx context.Context, provider *SQLProvider) (table string, result EncryptionValidationTableResult) {
var ( var (
rows *sqlx.Rows rows *sqlx.Rows
err error err error
) )
if rows, err = provider.db.QueryxContext(ctx, fmt.Sprintf(queryFmtSelectWebauthnDevicesEncryptedData, tableWebauthnDevices)); err != nil { if rows, err = provider.db.QueryxContext(ctx, fmt.Sprintf(queryFmtSelectWebAuthnDevicesEncryptedData, tableWebAuthnDevices)); err != nil {
return tableWebauthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error selecting Webauthn devices: %w", err)} return tableWebAuthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error selecting WebAuthn devices: %w", err)}
} }
var device encWebauthnDevice var device encWebAuthnDevice
for rows.Next() { for rows.Next() {
result.Total++ result.Total++
@ -280,7 +280,7 @@ func schemaEncryptionCheckKeyWebauthn(ctx context.Context, provider *SQLProvider
if err = rows.StructScan(&device); err != nil { if err = rows.StructScan(&device); err != nil {
_ = rows.Close() _ = rows.Close()
return tableWebauthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error scanning Webauthn device to struct: %w", err)} return tableWebAuthnDevices, EncryptionValidationTableResult{Error: fmt.Errorf("error scanning WebAuthn device to struct: %w", err)}
} }
if _, err = provider.decrypt(device.PublicKey); err != nil { if _, err = provider.decrypt(device.PublicKey); err != nil {
@ -290,7 +290,7 @@ func schemaEncryptionCheckKeyWebauthn(ctx context.Context, provider *SQLProvider
_ = rows.Close() _ = rows.Close()
return tableWebauthnDevices, result return tableWebAuthnDevices, result
} }
func schemaEncryptionCheckKeyOpenIDConnect(typeOAuth2Session OAuth2SessionType) EncryptionCheckKeyFunc { func schemaEncryptionCheckKeyOpenIDConnect(typeOAuth2Session OAuth2SessionType) EncryptionCheckKeyFunc {

View File

@ -119,59 +119,59 @@ const (
) )
const ( const (
queryFmtSelectWebauthnDevices = ` queryFmtSelectWebAuthnDevices = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
FROM %s FROM %s
LIMIT ? LIMIT ?
OFFSET ?;` OFFSET ?;`
queryFmtSelectWebauthnDevicesEncryptedData = ` queryFmtSelectWebAuthnDevicesEncryptedData = `
SELECT id, public_key SELECT id, public_key
FROM %s;` FROM %s;`
queryFmtSelectWebauthnDevicesByUsername = ` queryFmtSelectWebAuthnDevicesByUsername = `
SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning SELECT id, created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning
FROM %s FROM %s
WHERE username = ?;` WHERE username = ?;`
queryFmtUpdateWebauthnDevicePublicKey = ` queryFmtUpdateWebAuthnDevicePublicKey = `
UPDATE %s UPDATE %s
SET public_key = ? SET public_key = ?
WHERE id = ?;` WHERE id = ?;`
queryFmtUpdateWebauthnDeviceRecordSignIn = ` queryFmtUpdateWebAuthnDeviceRecordSignIn = `
UPDATE %s UPDATE %s
SET SET
rpid = ?, last_used_at = ?, sign_count = ?, rpid = ?, last_used_at = ?, sign_count = ?,
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
WHERE id = ?;` WHERE id = ?;`
queryFmtUpdateWebauthnDeviceRecordSignInByUsername = ` queryFmtUpdateWebAuthnDeviceRecordSignInByUsername = `
UPDATE %s UPDATE %s
SET SET
rpid = ?, last_used_at = ?, sign_count = ?, rpid = ?, last_used_at = ?, sign_count = ?,
clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END
WHERE username = ? AND kid = ?;` WHERE username = ? AND kid = ?;`
queryFmtUpsertWebauthnDevice = ` queryFmtUpsertWebAuthnDevice = `
REPLACE INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning) REPLACE INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
queryFmtUpsertWebauthnDevicePostgreSQL = ` queryFmtUpsertWebAuthnDevicePostgreSQL = `
INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning) INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (username, description) ON CONFLICT (username, description)
DO UPDATE SET created_at = $1, last_used_at = $2, rpid = $3, kid = $6, public_key = $7, attestation_type = $8, transport = $9, aaguid = $10, sign_count = $11, clone_warning = $12;` DO UPDATE SET created_at = $1, last_used_at = $2, rpid = $3, kid = $6, public_key = $7, attestation_type = $8, transport = $9, aaguid = $10, sign_count = $11, clone_warning = $12;`
queryFmtDeleteWebauthnDevice = ` queryFmtDeleteWebAuthnDevice = `
DELETE FROM %s DELETE FROM %s
WHERE kid = ?;` WHERE kid = ?;`
queryFmtDeleteWebauthnDeviceByUsername = ` queryFmtDeleteWebAuthnDeviceByUsername = `
DELETE FROM %s DELETE FROM %s
WHERE username = ?;` WHERE username = ?;`
queryFmtDeleteWebauthnDeviceByUsernameAndDescription = ` queryFmtDeleteWebAuthnDeviceByUsernameAndDescription = `
DELETE FROM %s DELETE FROM %s
WHERE username = ? AND description = ?;` WHERE username = ? AND description = ?;`
) )

View File

@ -32,7 +32,7 @@ type encOAuth2Session struct {
Session []byte `db:"session_data"` Session []byte `db:"session_data"`
} }
type encWebauthnDevice struct { type encWebAuthnDevice struct {
ID int `db:"id"` ID int `db:"id"`
PublicKey []byte `db:"public_key"` PublicKey []byte `db:"public_key"`
} }

View File

@ -12,7 +12,7 @@ import {
IndexRoute, IndexRoute,
LogoutRoute, LogoutRoute,
RegisterOneTimePasswordRoute, RegisterOneTimePasswordRoute,
RegisterWebauthnRoute, RegisterWebAuthnRoute,
ResetPasswordStep1Route, ResetPasswordStep1Route,
ResetPasswordStep2Route, ResetPasswordStep2Route,
} from "@constants/Routes"; } from "@constants/Routes";
@ -28,7 +28,7 @@ import {
getTheme, getTheme,
} from "@utils/Configuration"; } from "@utils/Configuration";
import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword"; import RegisterOneTimePassword from "@views/DeviceRegistration/RegisterOneTimePassword";
import RegisterWebauthn from "@views/DeviceRegistration/RegisterWebauthn"; import RegisterWebAuthn from "@views/DeviceRegistration/RegisterWebAuthn";
import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage"; import BaseLoadingPage from "@views/LoadingPage/BaseLoadingPage";
import ConsentView from "@views/LoginPortal/ConsentView/ConsentView"; import ConsentView from "@views/LoginPortal/ConsentView/ConsentView";
import LoginPortal from "@views/LoginPortal/LoginPortal"; import LoginPortal from "@views/LoginPortal/LoginPortal";
@ -89,7 +89,7 @@ const App: React.FC<Props> = (props: Props) => {
<Routes> <Routes>
<Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} /> <Route path={ResetPasswordStep1Route} element={<ResetPasswordStep1 />} />
<Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} /> <Route path={ResetPasswordStep2Route} element={<ResetPasswordStep2 />} />
<Route path={RegisterWebauthnRoute} element={<RegisterWebauthn />} /> <Route path={RegisterWebAuthnRoute} element={<RegisterWebAuthn />} />
<Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} /> <Route path={RegisterOneTimePasswordRoute} element={<RegisterOneTimePassword />} />
<Route path={LogoutRoute} element={<SignOut />} /> <Route path={LogoutRoute} element={<SignOut />} />
<Route path={ConsentRoute} element={<ConsentView />} /> <Route path={ConsentRoute} element={<ConsentView />} />

View File

@ -3,12 +3,12 @@ export const AuthenticatedRoute: string = "/authenticated";
export const ConsentRoute: string = "/consent"; export const ConsentRoute: string = "/consent";
export const SecondFactorRoute: string = "/2fa/"; export const SecondFactorRoute: string = "/2fa/";
export const SecondFactorWebauthnSubRoute: string = "webauthn"; export const SecondFactorWebAuthnSubRoute: string = "webauthn";
export const SecondFactorTOTPSubRoute: string = "one-time-password"; export const SecondFactorTOTPSubRoute: string = "one-time-password";
export const SecondFactorPushSubRoute: string = "push-notification"; export const SecondFactorPushSubRoute: string = "push-notification";
export const ResetPasswordStep1Route: string = "/reset-password/step1"; export const ResetPasswordStep1Route: string = "/reset-password/step1";
export const ResetPasswordStep2Route: string = "/reset-password/step2"; export const ResetPasswordStep2Route: string = "/reset-password/step2";
export const RegisterWebauthnRoute: string = "/webauthn/register"; export const RegisterWebAuthnRoute: string = "/webauthn/register";
export const RegisterOneTimePasswordRoute: string = "/one-time-password/register"; export const RegisterOneTimePasswordRoute: string = "/one-time-password/register";
export const LogoutRoute: string = "/logout"; export const LogoutRoute: string = "/logout";

View File

@ -11,11 +11,11 @@ export const FirstFactorPath = basePath + "/api/firstfactor";
export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start"; export const InitiateTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/start";
export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish"; export const CompleteTOTPRegistrationPath = basePath + "/api/secondfactor/totp/identity/finish";
export const WebauthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start"; export const WebAuthnIdentityStartPath = basePath + "/api/secondfactor/webauthn/identity/start";
export const WebauthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish"; export const WebAuthnIdentityFinishPath = basePath + "/api/secondfactor/webauthn/identity/finish";
export const WebauthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation"; export const WebAuthnAttestationPath = basePath + "/api/secondfactor/webauthn/attestation";
export const WebauthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion"; export const WebAuthnAssertionPath = basePath + "/api/secondfactor/webauthn/assertion";
export const InitiateDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_devices"; export const InitiateDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_devices";
export const CompleteDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_device"; export const CompleteDuoDeviceSelectionPath = basePath + "/api/secondfactor/duo_device";

View File

@ -1,4 +1,4 @@
import { CompleteTOTPRegistrationPath, InitiateTOTPRegistrationPath, WebauthnIdentityStartPath } from "@services/Api"; import { CompleteTOTPRegistrationPath, InitiateTOTPRegistrationPath, WebAuthnIdentityStartPath } from "@services/Api";
import { Post, PostWithOptionalResponse } from "@services/Client"; import { Post, PostWithOptionalResponse } from "@services/Client";
export async function initiateTOTPRegistrationProcess() { export async function initiateTOTPRegistrationProcess() {
@ -14,6 +14,6 @@ export async function completeTOTPRegistrationProcess(processToken: string) {
return Post<CompleteTOTPRegistrationResponse>(CompleteTOTPRegistrationPath, { token: processToken }); return Post<CompleteTOTPRegistrationResponse>(CompleteTOTPRegistrationPath, { token: processToken });
} }
export async function initiateWebauthnRegistrationProcess() { export async function initiateWebAuthnRegistrationProcess() {
return PostWithOptionalResponse(WebauthnIdentityStartPath); return PostWithOptionalResponse(WebAuthnIdentityStartPath);
} }

View File

@ -20,14 +20,14 @@ import {
import { import {
OptionalDataServiceResponse, OptionalDataServiceResponse,
ServiceResponse, ServiceResponse,
WebauthnAssertionPath, WebAuthnAssertionPath,
WebauthnAttestationPath, WebAuthnAttestationPath,
WebauthnIdentityFinishPath, WebAuthnIdentityFinishPath,
} from "@services/Api"; } from "@services/Api";
import { SignInResponse } from "@services/SignIn"; import { SignInResponse } from "@services/SignIn";
import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from "@utils/Base64"; import { getBase64WebEncodingFromBytes, getBytesFromBase64 } from "@utils/Base64";
export function isWebauthnSecure(): boolean { export function isWebAuthnSecure(): boolean {
if (window.isSecureContext) { if (window.isSecureContext) {
return true; return true;
} }
@ -35,12 +35,12 @@ export function isWebauthnSecure(): boolean {
return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"; return window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
} }
export function isWebauthnSupported(): boolean { export function isWebAuthnSupported(): boolean {
return window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function"; return window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === "function";
} }
export async function isWebauthnPlatformAuthenticatorAvailable(): Promise<boolean> { export async function isWebAuthnPlatformAuthenticatorAvailable(): Promise<boolean> {
if (!isWebauthnSupported()) { if (!isWebAuthnSupported()) {
return false; return false;
} }
@ -215,7 +215,7 @@ function getAssertionResultFromDOMException(
async function getAttestationCreationOptions(token: string): Promise<PublicKeyCredentialCreationOptionsStatus> { async function getAttestationCreationOptions(token: string): Promise<PublicKeyCredentialCreationOptionsStatus> {
let response: AxiosResponse<ServiceResponse<CredentialCreation>>; let response: AxiosResponse<ServiceResponse<CredentialCreation>>;
response = await axios.post<ServiceResponse<CredentialCreation>>(WebauthnIdentityFinishPath, { response = await axios.post<ServiceResponse<CredentialCreation>>(WebAuthnIdentityFinishPath, {
token: token, token: token,
}); });
@ -234,7 +234,7 @@ async function getAttestationCreationOptions(token: string): Promise<PublicKeyCr
export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> { export async function getAssertionRequestOptions(): Promise<PublicKeyCredentialRequestOptionsStatus> {
let response: AxiosResponse<ServiceResponse<CredentialRequest>>; let response: AxiosResponse<ServiceResponse<CredentialRequest>>;
response = await axios.get<ServiceResponse<CredentialRequest>>(WebauthnAssertionPath); response = await axios.get<ServiceResponse<CredentialRequest>>(WebAuthnAssertionPath);
if (response.data.status !== "OK" || response.data.data == null) { if (response.data.status !== "OK" || response.data.data == null) {
return { return {
@ -317,7 +317,7 @@ async function postAttestationPublicKeyCredentialResult(
): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> { ): Promise<AxiosResponse<OptionalDataServiceResponse<any>>> {
const credentialJSON = encodeAttestationPublicKeyCredential(credential); const credentialJSON = encodeAttestationPublicKeyCredential(credential);
return axios.post<OptionalDataServiceResponse<any>>(WebauthnAttestationPath, credentialJSON); return axios.post<OptionalDataServiceResponse<any>>(WebAuthnAttestationPath, credentialJSON);
} }
export async function postAssertionPublicKeyCredentialResult( export async function postAssertionPublicKeyCredentialResult(
@ -328,7 +328,7 @@ export async function postAssertionPublicKeyCredentialResult(
): Promise<AxiosResponse<ServiceResponse<SignInResponse>>> { ): Promise<AxiosResponse<ServiceResponse<SignInResponse>>> {
const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL, workflow, workflowID); const credentialJSON = encodeAssertionPublicKeyCredential(credential, targetURL, workflow, workflowID);
return axios.post<ServiceResponse<SignInResponse>>(WebauthnAssertionPath, credentialJSON); return axios.post<ServiceResponse<SignInResponse>>(WebAuthnAssertionPath, credentialJSON);
} }
export async function performAttestationCeremony(token: string): Promise<AttestationResult> { export async function performAttestationCeremony(token: string): Promise<AttestationResult> {

View File

@ -13,7 +13,7 @@ import { AttestationResult } from "@models/Webauthn";
import { FirstFactorPath } from "@services/Api"; import { FirstFactorPath } from "@services/Api";
import { performAttestationCeremony } from "@services/Webauthn"; import { performAttestationCeremony } from "@services/Webauthn";
const RegisterWebauthn = function () { const RegisterWebAuthn = function () {
const styles = useStyles(); const styles = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const { createErrorNotification } = useNotifications(); const { createErrorNotification } = useNotifications();
@ -99,7 +99,7 @@ const RegisterWebauthn = function () {
); );
}; };
export default RegisterWebauthn; export default RegisterWebAuthn;
const useStyles = makeStyles((theme: Theme) => ({ const useStyles = makeStyles((theme: Theme) => ({
icon: { icon: {

View File

@ -8,7 +8,7 @@ import {
SecondFactorPushSubRoute, SecondFactorPushSubRoute,
SecondFactorRoute, SecondFactorRoute,
SecondFactorTOTPSubRoute, SecondFactorTOTPSubRoute,
SecondFactorWebauthnSubRoute, SecondFactorWebAuthnSubRoute,
} from "@constants/Routes"; } from "@constants/Routes";
import { RedirectionURL } from "@constants/SearchParams"; import { RedirectionURL } from "@constants/SearchParams";
import { useConfiguration } from "@hooks/Configuration"; import { useConfiguration } from "@hooks/Configuration";
@ -144,7 +144,7 @@ const LoginPortal = function (props: Props) {
redirect(AuthenticatedRoute, false); redirect(AuthenticatedRoute, false);
} else { } else {
if (userInfo.method === SecondFactorMethod.Webauthn) { if (userInfo.method === SecondFactorMethod.Webauthn) {
redirect(`${SecondFactorRoute}${SecondFactorWebauthnSubRoute}`); redirect(`${SecondFactorRoute}${SecondFactorWebAuthnSubRoute}`);
} else if (userInfo.method === SecondFactorMethod.MobilePush) { } else if (userInfo.method === SecondFactorMethod.MobilePush) {
redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`); redirect(`${SecondFactorRoute}${SecondFactorPushSubRoute}`);
} else { } else {

View File

@ -8,7 +8,7 @@ import { Route, Routes, useNavigate } from "react-router-dom";
import { import {
SecondFactorPushSubRoute, SecondFactorPushSubRoute,
SecondFactorTOTPSubRoute, SecondFactorTOTPSubRoute,
SecondFactorWebauthnSubRoute, SecondFactorWebAuthnSubRoute,
LogoutRoute as SignOutRoute, LogoutRoute as SignOutRoute,
} from "@constants/Routes"; } from "@constants/Routes";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
@ -16,10 +16,10 @@ import LoginLayout from "@layouts/LoginLayout";
import { Configuration } from "@models/Configuration"; import { Configuration } from "@models/Configuration";
import { SecondFactorMethod } from "@models/Methods"; import { SecondFactorMethod } from "@models/Methods";
import { UserInfo } from "@models/UserInfo"; import { UserInfo } from "@models/UserInfo";
import { initiateTOTPRegistrationProcess, initiateWebauthnRegistrationProcess } from "@services/RegisterDevice"; import { initiateTOTPRegistrationProcess, initiateWebAuthnRegistrationProcess } from "@services/RegisterDevice";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
import { setPreferred2FAMethod } from "@services/UserInfo"; import { setPreferred2FAMethod } from "@services/UserInfo";
import { isWebauthnSupported } from "@services/Webauthn"; import { isWebAuthnSupported } from "@services/Webauthn";
import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog"; import MethodSelectionDialog from "@views/LoginPortal/SecondFactor/MethodSelectionDialog";
import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod"; import OneTimePasswordMethod from "@views/LoginPortal/SecondFactor/OneTimePasswordMethod";
import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod"; import PushNotificationMethod from "@views/LoginPortal/SecondFactor/PushNotificationMethod";
@ -45,7 +45,7 @@ const SecondFactorForm = function (props: Props) {
const { t: translate } = useTranslation(); const { t: translate } = useTranslation();
useEffect(() => { useEffect(() => {
setWebauthnSupported(isWebauthnSupported()); setWebauthnSupported(isWebAuthnSupported());
}, [setWebauthnSupported]); }, [setWebauthnSupported]);
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => { const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => {
@ -124,14 +124,14 @@ const SecondFactorForm = function (props: Props) {
} }
/> />
<Route <Route
path={SecondFactorWebauthnSubRoute} path={SecondFactorWebAuthnSubRoute}
element={ element={
<WebauthnMethod <WebauthnMethod
id="webauthn-method" id="webauthn-method"
authenticationLevel={props.authenticationLevel} authenticationLevel={props.authenticationLevel}
// Whether the user has a Webauthn device registered already // Whether the user has a Webauthn device registered already
registered={props.userInfo.has_webauthn} registered={props.userInfo.has_webauthn}
onRegisterClick={initiateRegistration(initiateWebauthnRegistrationProcess)} onRegisterClick={initiateRegistration(initiateWebAuthnRegistrationProcess)}
onSignInError={(err) => createErrorNotification(err.message)} onSignInError={(err) => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} onSignInSuccess={props.onAuthenticationSuccess}
/> />