From 24e41aed845d5f06a26444bb154e22e1b41bba8d Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 19 Oct 2022 18:17:55 +1100 Subject: [PATCH] feat(commands): add webauthn device commands (#3671) --- .../cli/authelia/authelia_storage_user.md | 1 + .../authelia_storage_user_webauthn.md | 65 +++++++ .../authelia_storage_user_webauthn_delete.md | 78 ++++++++ .../authelia_storage_user_webauthn_list.md | 72 +++++++ internal/commands/const.go | 37 ++++ internal/commands/storage.go | 53 +++++ internal/commands/storage_run.go | 181 ++++++++++++++++++ internal/mocks/storage.go | 28 +++ internal/storage/provider.go | 2 + internal/storage/sql_provider.go | 36 ++++ .../storage/sql_provider_backend_postgres.go | 3 + internal/storage/sql_provider_queries.go | 26 ++- 12 files changed, 575 insertions(+), 7 deletions(-) create mode 100644 docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn.md create mode 100644 docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_delete.md create mode 100644 docs/content/en/reference/cli/authelia/authelia_storage_user_webauthn_list.md 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`