diff options
| author | James Elliott <james-d-elliott@users.noreply.github.com> | 2022-10-19 18:17:55 +1100 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-19 18:17:55 +1100 | 
| commit | 24e41aed845d5f06a26444bb154e22e1b41bba8d (patch) | |
| tree | 6182ec17b43a182bfa194196c887438701b55013 | |
| parent | 52102eea8c7379e0d34d9025ea72bebdcf639673 (diff) | |
feat(commands): add webauthn device commands (#3671)
| -rw-r--r-- | docs/content/en/reference/cli/authelia/authelia_storage_user.md | 1 | ||||
| -rw-r--r-- | docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn.md | 65 | ||||
| -rw-r--r-- | docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_delete.md | 78 | ||||
| -rw-r--r-- | docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_list.md | 72 | ||||
| -rw-r--r-- | internal/commands/const.go | 37 | ||||
| -rw-r--r-- | internal/commands/storage.go | 53 | ||||
| -rw-r--r-- | internal/commands/storage_run.go | 181 | ||||
| -rw-r--r-- | internal/mocks/storage.go | 28 | ||||
| -rw-r--r-- | internal/storage/provider.go | 2 | ||||
| -rw-r--r-- | internal/storage/sql_provider.go | 36 | ||||
| -rw-r--r-- | internal/storage/sql_provider_backend_postgres.go | 3 | ||||
| -rw-r--r-- | internal/storage/sql_provider_queries.go | 26 | 
12 files changed, 575 insertions, 7 deletions
diff --git a/docs/content/en/reference/cli/authelia/authelia_storage_user.md b/docs/content/en/reference/cli/authelia/authelia_storage_user.md index 14561dddb..90fc3c917 100644 --- a/docs/content/en/reference/cli/authelia/authelia_storage_user.md +++ b/docs/content/en/reference/cli/authelia/authelia_storage_user.md @@ -62,4 +62,5 @@ authelia storage user --help  * [authelia storage](authelia_storage.md)	 - Manage the Authelia storage  * [authelia storage user identifiers](authelia_storage_user_identifiers.md)	 - Manage user opaque identifiers  * [authelia storage user totp](authelia_storage_user_totp.md)	 - Manage TOTP configurations +* [authelia storage user webauthn](authelia_storage_user_webauthn.md)	 - Manage Webauthn devices diff --git a/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn.md b/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn.md new file mode 100644 index 000000000..65ee8c777 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn.md @@ -0,0 +1,65 @@ +--- +title: "authelia storage user webauthn" +description: "Reference for the authelia storage user webauthn command." +lead: "" +date: 2022-06-15T17:51:47+10:00 +draft: false +images: [] +menu: +  reference: +    parent: "cli-authelia" +weight: 330 +toc: true +--- + +## authelia storage user webauthn + +Manage Webauthn devices + +### Synopsis + +Manage Webauthn devices. + +This subcommand allows interacting with Webauthn devices. + +### Examples + +``` +authelia storage user webauthn --help +``` + +### Options + +``` +  -h, --help   help for webauthn +``` + +### Options inherited from parent commands + +``` +  -c, --config strings                         configuration files to load (default [configuration.yml]) +      --encryption-key string                  the storage encryption key to use +      --mysql.database string                  the MySQL database name (default "authelia") +      --mysql.host string                      the MySQL hostname +      --mysql.password string                  the MySQL password +      --mysql.port int                         the MySQL port (default 3306) +      --mysql.username string                  the MySQL username (default "authelia") +      --postgres.database string               the PostgreSQL database name (default "authelia") +      --postgres.host string                   the PostgreSQL hostname +      --postgres.password string               the PostgreSQL password +      --postgres.port int                      the PostgreSQL port (default 5432) +      --postgres.schema string                 the PostgreSQL schema name (default "public") +      --postgres.ssl.certificate string        the PostgreSQL ssl certificate file location +      --postgres.ssl.key string                the PostgreSQL ssl key file location +      --postgres.ssl.mode string               the PostgreSQL ssl mode (default "disable") +      --postgres.ssl.root_certificate string   the PostgreSQL ssl root certificate file location +      --postgres.username string               the PostgreSQL username (default "authelia") +      --sqlite.path string                     the SQLite database path +``` + +### SEE ALSO + +* [authelia storage user](authelia_storage_user.md)	 - Manages user settings +* [authelia storage user webauthn delete](authelia_storage_user_webauthn_delete.md)	 - Delete a WebAuthn device +* [authelia storage user webauthn list](authelia_storage_user_webauthn_list.md)	 - List WebAuthn devices + diff --git a/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_delete.md b/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_delete.md new file mode 100644 index 000000000..cf2545624 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_delete.md @@ -0,0 +1,78 @@ +--- +title: "authelia storage user webauthn delete" +description: "Reference for the authelia storage user webauthn delete command." +lead: "" +date: 2022-06-15T17:51:47+10:00 +draft: false +images: [] +menu: +  reference: +    parent: "cli-authelia" +weight: 330 +toc: true +--- + +## authelia storage user webauthn delete + +Delete a WebAuthn device + +### Synopsis + +Delete a WebAuthn device. + +This subcommand allows deleting a WebAuthn device directly from the database. + +``` +authelia storage user webauthn delete [username] [flags] +``` + +### Examples + +``` +authelia storage user webauthn delete john --all +authelia storage user webauthn delete john --all --config config.yml +authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +authelia storage user webauthn delete john --description Primary +authelia storage user webauthn delete john --description Primary --config config.yml +authelia storage user webauthn delete john --description Primary --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +authelia storage user webauthn delete --kid abc123 +authelia storage user webauthn delete --kid abc123 --config config.yml +authelia storage user webauthn delete --kid abc123 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +``` + +### Options + +``` +      --all                  delete all of the users webauthn devices +      --description string   delete a users webauthn device by description +  -h, --help                 help for delete +      --kid string           delete a users webauthn device by key id +``` + +### Options inherited from parent commands + +``` +  -c, --config strings                         configuration files to load (default [configuration.yml]) +      --encryption-key string                  the storage encryption key to use +      --mysql.database string                  the MySQL database name (default "authelia") +      --mysql.host string                      the MySQL hostname +      --mysql.password string                  the MySQL password +      --mysql.port int                         the MySQL port (default 3306) +      --mysql.username string                  the MySQL username (default "authelia") +      --postgres.database string               the PostgreSQL database name (default "authelia") +      --postgres.host string                   the PostgreSQL hostname +      --postgres.password string               the PostgreSQL password +      --postgres.port int                      the PostgreSQL port (default 5432) +      --postgres.schema string                 the PostgreSQL schema name (default "public") +      --postgres.ssl.certificate string        the PostgreSQL ssl certificate file location +      --postgres.ssl.key string                the PostgreSQL ssl key file location +      --postgres.ssl.mode string               the PostgreSQL ssl mode (default "disable") +      --postgres.ssl.root_certificate string   the PostgreSQL ssl root certificate file location +      --postgres.username string               the PostgreSQL username (default "authelia") +      --sqlite.path string                     the SQLite database path +``` + +### SEE ALSO + +* [authelia storage user webauthn](authelia_storage_user_webauthn.md)	 - Manage Webauthn devices + diff --git a/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_list.md b/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_list.md new file mode 100644 index 000000000..185e8af15 --- /dev/null +++ b/docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_list.md @@ -0,0 +1,72 @@ +--- +title: "authelia storage user webauthn list" +description: "Reference for the authelia storage user webauthn list command." +lead: "" +date: 2022-06-15T17:51:47+10:00 +draft: false +images: [] +menu: +  reference: +    parent: "cli-authelia" +weight: 330 +toc: true +--- + +## authelia storage user webauthn list + +List WebAuthn devices + +### Synopsis + +List WebAuthn devices. + +This subcommand allows listing WebAuthn devices. + +``` +authelia storage user webauthn list [username] [flags] +``` + +### Examples + +``` +authelia storage user webauthn list +authelia storage user webauthn list john +authelia storage user webauthn list --config config.yml +authelia storage user webauthn list john --config config.yml +authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +``` + +### Options + +``` +  -h, --help   help for list +``` + +### Options inherited from parent commands + +``` +  -c, --config strings                         configuration files to load (default [configuration.yml]) +      --encryption-key string                  the storage encryption key to use +      --mysql.database string                  the MySQL database name (default "authelia") +      --mysql.host string                      the MySQL hostname +      --mysql.password string                  the MySQL password +      --mysql.port int                         the MySQL port (default 3306) +      --mysql.username string                  the MySQL username (default "authelia") +      --postgres.database string               the PostgreSQL database name (default "authelia") +      --postgres.host string                   the PostgreSQL hostname +      --postgres.password string               the PostgreSQL password +      --postgres.port int                      the PostgreSQL port (default 5432) +      --postgres.schema string                 the PostgreSQL schema name (default "public") +      --postgres.ssl.certificate string        the PostgreSQL ssl certificate file location +      --postgres.ssl.key string                the PostgreSQL ssl key file location +      --postgres.ssl.mode string               the PostgreSQL ssl mode (default "disable") +      --postgres.ssl.root_certificate string   the PostgreSQL ssl root certificate file location +      --postgres.username string               the PostgreSQL username (default "authelia") +      --sqlite.path string                     the SQLite database path +``` + +### SEE ALSO + +* [authelia storage user webauthn](authelia_storage_user_webauthn.md)	 - Manage Webauthn devices + diff --git a/internal/commands/const.go b/internal/commands/const.go index b4fb7986b..13bb8aa24 100644 --- a/internal/commands/const.go +++ b/internal/commands/const.go @@ -176,6 +176,43 @@ This subcommand allows manually adding an opaque identifier for a user to the da  authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --config config.yml  authelia storage user identifiers add john --identifier f0919359-9d15-4e15-bcba-83b41620a073 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` +	cmdAutheliaStorageUserWebAuthnShort = "Manage Webauthn devices" + +	cmdAutheliaStorageUserWebAuthnLong = `Manage Webauthn devices. + +This subcommand allows interacting with Webauthn devices.` + +	cmdAutheliaStorageUserWebAuthnExample = `authelia storage user webauthn --help` + +	cmdAutheliaStorageUserWebAuthnListShort = "List WebAuthn devices" + +	cmdAutheliaStorageUserWebAuthnListLong = `List WebAuthn devices. + +This subcommand allows listing WebAuthn devices.` + +	cmdAutheliaStorageUserWebAuthnListExample = `authelia storage user webauthn list +authelia storage user webauthn list john +authelia storage user webauthn list --config config.yml +authelia storage user webauthn list john --config config.yml +authelia storage user webauthn list --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +authelia storage user webauthn list john --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` + +	cmdAutheliaStorageUserWebAuthnDeleteShort = "Delete a WebAuthn device" + +	cmdAutheliaStorageUserWebAuthnDeleteLong = `Delete a WebAuthn device. + +This subcommand allows deleting a WebAuthn device directly from the database.` + +	cmdAutheliaStorageUserWebAuthnDeleteExample = `authelia storage user webauthn delete john --all +authelia storage user webauthn delete john --all --config config.yml +authelia storage user webauthn delete john --all --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +authelia storage user webauthn delete john --description Primary +authelia storage user webauthn delete john --description Primary --config config.yml +authelia storage user webauthn delete john --description Primary --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw +authelia storage user webauthn delete --kid abc123 +authelia storage user webauthn delete --kid abc123 --config config.yml +authelia storage user webauthn delete --kid abc123 --encryption-key b3453fde-ecc2-4a1f-9422-2707ddbed495 --postgres.host postgres --postgres.password autheliapw` +  	cmdAutheliaStorageUserTOTPShort = "Manage TOTP configurations"  	cmdAutheliaStorageUserTOTPLong = `Manage TOTP configurations. diff --git a/internal/commands/storage.go b/internal/commands/storage.go index adfd1c28c..92190149a 100644 --- a/internal/commands/storage.go +++ b/internal/commands/storage.go @@ -117,6 +117,7 @@ func newStorageUserCmd() (cmd *cobra.Command) {  	cmd.AddCommand(  		newStorageUserIdentifiersCmd(),  		newStorageUserTOTPCmd(), +		newStorageUserWebAuthnCmd(),  	)  	return cmd @@ -211,6 +212,58 @@ func newStorageUserIdentifiersAddCmd() (cmd *cobra.Command) {  	return cmd  } +func newStorageUserWebAuthnCmd() (cmd *cobra.Command) { +	cmd = &cobra.Command{ +		Use:     "webauthn", +		Short:   cmdAutheliaStorageUserWebAuthnShort, +		Long:    cmdAutheliaStorageUserWebAuthnLong, +		Example: cmdAutheliaStorageUserWebAuthnExample, + +		DisableAutoGenTag: true, +	} + +	cmd.AddCommand( +		newStorageUserWebAuthnListCmd(), +		newStorageUserWebAuthnDeleteCmd(), +	) + +	return cmd +} + +func newStorageUserWebAuthnListCmd() (cmd *cobra.Command) { +	cmd = &cobra.Command{ +		Use:     "list [username]", +		Short:   cmdAutheliaStorageUserWebAuthnListShort, +		Long:    cmdAutheliaStorageUserWebAuthnListLong, +		Example: cmdAutheliaStorageUserWebAuthnListExample, +		RunE:    storageWebAuthnListRunE, +		Args:    cobra.MaximumNArgs(1), + +		DisableAutoGenTag: true, +	} + +	return cmd +} + +func newStorageUserWebAuthnDeleteCmd() (cmd *cobra.Command) { +	cmd = &cobra.Command{ +		Use:     "delete [username]", +		Short:   cmdAutheliaStorageUserWebAuthnDeleteShort, +		Long:    cmdAutheliaStorageUserWebAuthnDeleteLong, +		Example: cmdAutheliaStorageUserWebAuthnDeleteExample, +		RunE:    storageWebAuthnDeleteRunE, +		Args:    cobra.MaximumNArgs(1), + +		DisableAutoGenTag: true, +	} + +	cmd.Flags().Bool("all", false, "delete all of the users webauthn devices") +	cmd.Flags().String("description", "", "delete a users webauthn device by description") +	cmd.Flags().String("kid", "", "delete a users webauthn device by key id") + +	return cmd +} +  func newStorageUserTOTPCmd() (cmd *cobra.Command) {  	cmd = &cobra.Command{  		Use:     "totp", diff --git a/internal/commands/storage_run.go b/internal/commands/storage_run.go index 50b40a876..4b11c5e72 100644 --- a/internal/commands/storage_run.go +++ b/internal/commands/storage_run.go @@ -205,6 +205,187 @@ func storageSchemaEncryptionChangeKeyRunE(cmd *cobra.Command, args []string) (er  	return nil  } +func storageWebAuthnListRunE(cmd *cobra.Command, args []string) (err error) { +	if len(args) == 0 || args[0] == "" { +		return storageWebAuthnListAllRunE(cmd, args) +	} + +	var ( +		provider storage.Provider +		ctx      = context.Background() +	) + +	provider = getStorageProvider() + +	defer func() { +		_ = provider.Close() +	}() + +	var devices []model.WebauthnDevice + +	user := args[0] + +	devices, err = provider.LoadWebauthnDevicesByUsername(ctx, user) + +	switch { +	case len(devices) == 0 || (err != nil && errors.Is(err, storage.ErrNoWebauthnDevice)): +		return fmt.Errorf("user '%s' has no webauthn devices", user) +	case err != nil: +		return fmt.Errorf("can't list devices for user '%s': %w", user, err) +	default: +		fmt.Printf("Webauthn Devices for user '%s':\n\n", user) +		fmt.Printf("ID\tKID\tDescription\n") + +		for _, device := range devices { +			fmt.Printf("%d\t%s\t%s", device.ID, device.KID, device.Description) +		} +	} + +	return nil +} + +func storageWebAuthnListAllRunE(_ *cobra.Command, _ []string) (err error) { +	var ( +		provider storage.Provider +		ctx      = context.Background() +	) + +	provider = getStorageProvider() + +	defer func() { +		_ = provider.Close() +	}() + +	var devices []model.WebauthnDevice + +	limit := 10 + +	output := strings.Builder{} + +	for page := 0; true; page++ { +		if devices, err = provider.LoadWebauthnDevices(ctx, limit, page); err != nil { +			return fmt.Errorf("failed to list devices: %w", err) +		} + +		if page == 0 && len(devices) == 0 { +			return errors.New("no webauthn devices in database") +		} + +		for _, device := range devices { +			output.WriteString(fmt.Sprintf("%d\t%s\t%s\t%s\n", device.ID, device.KID, device.Description, device.Username)) +		} + +		if len(devices) < limit { +			break +		} +	} + +	fmt.Printf("Webauthn Devices:\n\nID\tKID\tDescription\tUsername\n") +	fmt.Println(output.String()) + +	return nil +} + +func storageWebAuthnDeleteRunE(cmd *cobra.Command, args []string) (err error) { +	var ( +		provider storage.Provider +		ctx      = context.Background() +	) + +	provider = getStorageProvider() + +	defer func() { +		_ = provider.Close() +	}() + +	var ( +		all, byKID             bool +		description, kid, user string +	) + +	if all, byKID, description, kid, user, err = storageWebAuthnDeleteGetAndValidateConfig(cmd, args); err != nil { +		return err +	} + +	if byKID { +		if err = provider.DeleteWebauthnDevice(ctx, kid); err != nil { +			return fmt.Errorf("failed to delete WebAuthn device with kid '%s': %w", kid, err) +		} + +		fmt.Printf("Deleted WebAuthn device with kid '%s'", kid) +	} else { +		err = provider.DeleteWebauthnDeviceByUsername(ctx, user, description) + +		if all { +			if err != nil { +				return fmt.Errorf("failed to delete all WebAuthn devices with username '%s': %w", user, err) +			} + +			fmt.Printf("Deleted all WebAuthn devices for user '%s'", user) +		} else { +			if err != nil { +				return fmt.Errorf("failed to delete WebAuthn device with username '%s' and description '%s': %w", user, description, err) +			} + +			fmt.Printf("Deleted WebAuthn device with username '%s' and description '%s'", user, description) +		} +	} + +	return nil +} + +func storageWebAuthnDeleteGetAndValidateConfig(cmd *cobra.Command, args []string) (all, byKID bool, description, kid, user string, err error) { +	if len(args) != 0 { +		user = args[0] +	} + +	flags := 0 + +	if cmd.Flags().Changed("all") { +		if all, err = cmd.Flags().GetBool("all"); err != nil { +			return +		} + +		flags++ +	} + +	if cmd.Flags().Changed("description") { +		if description, err = cmd.Flags().GetString("description"); err != nil { +			return +		} + +		flags++ +	} + +	if byKID = cmd.Flags().Changed("kid"); byKID { +		if kid, err = cmd.Flags().GetString("kid"); err != nil { +			return +		} + +		flags++ +	} + +	if flags > 1 { +		err = fmt.Errorf("must only supply one of the flags --all, --description, and --kid but %d were specified", flags) + +		return +	} + +	if flags == 0 { +		err = fmt.Errorf("must supply one of the flags --all, --description, or --kid") + +		return +	} + +	if !byKID && len(user) == 0 { +		err = fmt.Errorf("must supply the username or the --kid flag") + +		return +	} + +	return +} +  func storageTOTPGenerateRunE(cmd *cobra.Command, args []string) (err error) {  	var (  		provider         storage.Provider diff --git a/internal/mocks/storage.go b/internal/mocks/storage.go index 2f6160d76..980469a35 100644 --- a/internal/mocks/storage.go +++ b/internal/mocks/storage.go @@ -166,6 +166,34 @@ func (mr *MockStorageMockRecorder) DeleteTOTPConfiguration(arg0, arg1 interface{  	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).DeleteTOTPConfiguration), arg0, arg1)  } +// DeleteWebauthnDevice mocks base method. +func (m *MockStorage) DeleteWebauthnDevice(arg0 context.Context, arg1 string) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "DeleteWebauthnDevice", arg0, arg1) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// DeleteWebauthnDevice indicates an expected call of DeleteWebauthnDevice. +func (mr *MockStorageMockRecorder) DeleteWebauthnDevice(arg0, arg1 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDevice", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDevice), arg0, arg1) +} + +// DeleteWebauthnDeviceByUsername mocks base method. +func (m *MockStorage) DeleteWebauthnDeviceByUsername(arg0 context.Context, arg1, arg2 string) error { +	m.ctrl.T.Helper() +	ret := m.ctrl.Call(m, "DeleteWebauthnDeviceByUsername", arg0, arg1, arg2) +	ret0, _ := ret[0].(error) +	return ret0 +} + +// DeleteWebauthnDeviceByUsername indicates an expected call of DeleteWebauthnDeviceByUsername. +func (mr *MockStorageMockRecorder) DeleteWebauthnDeviceByUsername(arg0, arg1, arg2 interface{}) *gomock.Call { +	mr.mock.ctrl.T.Helper() +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWebauthnDeviceByUsername", reflect.TypeOf((*MockStorage)(nil).DeleteWebauthnDeviceByUsername), arg0, arg1, arg2) +} +  // FindIdentityVerification mocks base method.  func (m *MockStorage) FindIdentityVerification(arg0 context.Context, arg1 string) (bool, error) {  	m.ctrl.T.Helper() diff --git a/internal/storage/provider.go b/internal/storage/provider.go index 7696966a5..fdf415a89 100644 --- a/internal/storage/provider.go +++ b/internal/storage/provider.go @@ -39,6 +39,8 @@ type Provider interface {  	SaveWebauthnDevice(ctx context.Context, device model.WebauthnDevice) (err error)  	UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rpid string, lastUsedAt *time.Time, signCount uint32, cloneWarning bool) (err error) +	DeleteWebauthnDevice(ctx context.Context, kid string) (err error) +	DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (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) diff --git a/internal/storage/sql_provider.go b/internal/storage/sql_provider.go index bb5ac9895..6991e7021 100644 --- a/internal/storage/sql_provider.go +++ b/internal/storage/sql_provider.go @@ -56,6 +56,10 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa  		sqlUpdateWebauthnDeviceRecordSignIn:           fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignIn, tableWebauthnDevices),  		sqlUpdateWebauthnDeviceRecordSignInByUsername: fmt.Sprintf(queryFmtUpdateWebauthnDeviceRecordSignInByUsername, tableWebauthnDevices), +		sqlDeleteWebauthnDevice:                         fmt.Sprintf(queryFmtDeleteWebauthnDevice, tableWebauthnDevices), +		sqlDeleteWebauthnDeviceByUsername:               fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsername, tableWebauthnDevices), +		sqlDeleteWebauthnDeviceByUsernameAndDescription: fmt.Sprintf(queryFmtDeleteWebauthnDeviceByUsernameAndDescription, tableWebauthnDevices), +  		sqlUpsertDuoDevice: fmt.Sprintf(queryFmtUpsertDuoDevice, tableDuoDevices),  		sqlDeleteDuoDevice: fmt.Sprintf(queryFmtDeleteDuoDevice, tableDuoDevices),  		sqlSelectDuoDevice: fmt.Sprintf(queryFmtSelectDuoDevice, tableDuoDevices), @@ -169,6 +173,10 @@ type SQLProvider struct {  	sqlUpdateWebauthnDeviceRecordSignIn           string  	sqlUpdateWebauthnDeviceRecordSignInByUsername string +	sqlDeleteWebauthnDevice                         string +	sqlDeleteWebauthnDeviceByUsername               string +	sqlDeleteWebauthnDeviceByUsernameAndDescription string +  	// Table: duo_devices.  	sqlUpsertDuoDevice string  	sqlDeleteDuoDevice string @@ -841,6 +849,34 @@ func (p *SQLProvider) UpdateWebauthnDeviceSignIn(ctx context.Context, id int, rp  	return nil  } +// DeleteWebauthnDevice deletes a registered Webauthn device. +func (p *SQLProvider) DeleteWebauthnDevice(ctx context.Context, kid string) (err error) { +	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 nil +} + +// DeleteWebauthnDeviceByUsername deletes registered Webauthn devices by username or username and description. +func (p *SQLProvider) DeleteWebauthnDeviceByUsername(ctx context.Context, username, description string) (err error) { +	if len(username) == 0 { +		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 _, err = p.db.ExecContext(ctx, p.sqlDeleteWebauthnDeviceByUsername, username); err != nil { +			return fmt.Errorf("error deleting webauthn devices for username '%s': %w", username, err) +		} +	} else { +		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 nil +} +  // LoadWebauthnDevices loads Webauthn device registrations.  func (p *SQLProvider) LoadWebauthnDevices(ctx context.Context, limit, page int) (devices []model.WebauthnDevice, err error) {  	devices = make([]model.WebauthnDevice, 0, limit) diff --git a/internal/storage/sql_provider_backend_postgres.go b/internal/storage/sql_provider_backend_postgres.go index 3b98e64e4..06fc40c78 100644 --- a/internal/storage/sql_provider_backend_postgres.go +++ b/internal/storage/sql_provider_backend_postgres.go @@ -61,6 +61,9 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr  	provider.sqlUpdateWebauthnDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKeyByUsername)  	provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn)  	provider.sqlUpdateWebauthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignInByUsername) +	provider.sqlDeleteWebauthnDevice = provider.db.Rebind(provider.sqlDeleteWebauthnDevice) +	provider.sqlDeleteWebauthnDeviceByUsername = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsername) +	provider.sqlDeleteWebauthnDeviceByUsernameAndDescription = provider.db.Rebind(provider.sqlDeleteWebauthnDeviceByUsernameAndDescription)  	provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)  	provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice) diff --git a/internal/storage/sql_provider_queries.go b/internal/storage/sql_provider_queries.go index 7dc44e156..eedfca30a 100644 --- a/internal/storage/sql_provider_queries.go +++ b/internal/storage/sql_provider_queries.go @@ -122,13 +122,13 @@ const (  const (  	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  		LIMIT ?  		OFFSET ?;`  	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  		WHERE username = ?;` @@ -144,14 +144,14 @@ const (  	queryFmtUpdateWebauthnDeviceRecordSignIn = `  		UPDATE %s -		SET  +		SET  			rpid = ?, last_used_at = ?, sign_count = ?,  			clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END  		WHERE id = ?;`  	queryFmtUpdateWebauthnDeviceRecordSignInByUsername = `  		UPDATE %s -		SET  +		SET  			rpid = ?, last_used_at = ?, sign_count = ?,  			clone_warning = CASE clone_warning WHEN TRUE THEN TRUE ELSE ? END  		WHERE username = ? AND kid = ?;` @@ -165,6 +165,18 @@ const (  		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)  			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;` + +	queryFmtDeleteWebauthnDevice = ` +		DELETE FROM %s +		WHERE kid = ?;` + +	queryFmtDeleteWebauthnDeviceByUsername = ` +		DELETE FROM %s +		WHERE username = ?;` + +	queryFmtDeleteWebauthnDeviceByUsernameAndDescription = ` +		DELETE FROM %s +		WHERE username = ? AND description = ?;`  )  const ( @@ -232,7 +244,7 @@ const (  		SELECT id, challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at,  		form_data, requested_scopes, granted_scopes, requested_audience, granted_audience  		FROM %s -		WHERE client_id = ? AND subject = ? AND  +		WHERE client_id = ? AND subject = ? AND  			  authorized = TRUE AND granted = TRUE AND expires_at IS NOT NULL AND expires_at >= CURRENT_TIMESTAMP;`  	queryFmtInsertOAuth2ConsentSession = ` @@ -263,8 +275,8 @@ const (  		WHERE signature = ? AND revoked = FALSE;`  	queryFmtInsertOAuth2Session = ` -		INSERT INTO %s (challenge_id, request_id, client_id, signature, subject, requested_at,  -		requested_scopes, granted_scopes, requested_audience, granted_audience,  +		INSERT INTO %s (challenge_id, request_id, client_id, signature, subject, requested_at, +		requested_scopes, granted_scopes, requested_audience, granted_audience,  		active, revoked, form_data, session_data)  		VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`  | 
