From 2e8505a84a9cc3058f4eddb03fecbeb447355ad2 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 14:44:36 +0800 Subject: [PATCH 001/180] db: add initial schema --- .../20220607041349_add_mfa_schema.up.sql | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 migrations/20220607041349_add_mfa_schema.up.sql diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql new file mode 100644 index 000000000..8c0ef0a58 --- /dev/null +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -0,0 +1,49 @@ +-- auth.backups definition + +DROP TYPE IF EXISTS factor_type; +CREATE TYPE factor_type AS ENUM('phone', 'webauthn', 'email'); + +-- auth.factors definition + +CREATE TABLE IF NOT EXISTS auth.mfa_factors( + id VARCHAR(256) NOT NULL, + user_id uuid NOT NULL, + factor_simple_name VARCHAR(256) NULL, + factor_type factor_type NOT NULL, + enabled BOOLEAN NOT NULL, + created_at timestamptz NOT NULL, + updated_at timestamptz NULL, + secret_key VARCHAR(256) NULL, + CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), + CONSTRAINT mfa_factors FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE +); + +comment on table auth.mfa_factors is 'Auth: stores Multi Factor Authentication factor data'; + +CREATE TABLE IF NOT EXISTS auth.mfa_challenges( + id VARCHAR(256) NOT NULL, + factor_id VARCHAR(256) NOT NULL, + created_at timestamptz NULL, + CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id), + CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE +); + +comment on table auth.mfa_challenges is 'Auth: stores data of Multi Factor Authentication Requests'; + + +-- auth.challenge definition +CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( + user_id uuid NOT NULL, + created_at timestamptz NOT NULL, + backup_code VARCHAR(32) NOT NULL, + valid BOOLEAN NOT NULL, + time_used timestamptz NULL, + CONSTRAINT mfa_backup_codes_pkey PRIMARY KEY(user_id, backup_code), + CONSTRAINT mfa_backup_codes FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE +); + +comment on table auth.mfa_backup_codes is 'Auth: stores backup codes for Multi Factor Authentication'; + +-- Add MFA toggle on Users table +ALTER TABLE auth.users +ADD COLUMN IF NOT EXISTS mfa_enabled boolean NULL; From 14107337d9add5bbe1227e5ac24c3bf224cf3775 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 14:53:40 +0800 Subject: [PATCH 002/180] fix: update migrations --- .../20220607041349_add_mfa_schema.up.sql | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 8c0ef0a58..0e2de90cf 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -1,10 +1,11 @@ --- auth.backups definition - -DROP TYPE IF EXISTS factor_type; -CREATE TYPE factor_type AS ENUM('phone', 'webauthn', 'email'); - --- auth.factors definition - +-- See: https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql/48382296#48382296 +DO $$ BEGIN + CREATE TYPE factor_type AS ENUM('phone', 'webauthn', 'email'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- auth.mfa_factors definition CREATE TABLE IF NOT EXISTS auth.mfa_factors( id VARCHAR(256) NOT NULL, user_id uuid NOT NULL, @@ -13,37 +14,34 @@ CREATE TABLE IF NOT EXISTS auth.mfa_factors( enabled BOOLEAN NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NULL, - secret_key VARCHAR(256) NULL, + secret_key VARCHAR(256) NOT NULL, CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), CONSTRAINT mfa_factors FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); - comment on table auth.mfa_factors is 'Auth: stores Multi Factor Authentication factor data'; +-- auth.mfa_challenges definition CREATE TABLE IF NOT EXISTS auth.mfa_challenges( id VARCHAR(256) NOT NULL, factor_id VARCHAR(256) NOT NULL, - created_at timestamptz NULL, + created_at timestamptz NOT NULL, CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id), CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE ); - comment on table auth.mfa_challenges is 'Auth: stores data of Multi Factor Authentication Requests'; - --- auth.challenge definition +-- auth.mfa_backup_codes definition CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( user_id uuid NOT NULL, created_at timestamptz NOT NULL, backup_code VARCHAR(32) NOT NULL, valid BOOLEAN NOT NULL, - time_used timestamptz NULL, + time_used timestamptz NOT NULL, CONSTRAINT mfa_backup_codes_pkey PRIMARY KEY(user_id, backup_code), CONSTRAINT mfa_backup_codes FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); - comment on table auth.mfa_backup_codes is 'Auth: stores backup codes for Multi Factor Authentication'; -- Add MFA toggle on Users table ALTER TABLE auth.users -ADD COLUMN IF NOT EXISTS mfa_enabled boolean NULL; +ADD COLUMN IF NOT EXISTS mfa_enabled boolean NOT NULL; From 9050cf845161214d94837854c5ba5459108ac0e5 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 15:00:23 +0800 Subject: [PATCH 003/180] fix: add additional constraints --- migrations/20220607041349_add_mfa_schema.up.sql | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 0e2de90cf..4dbc0ba1e 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -30,13 +30,14 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( ); comment on table auth.mfa_challenges is 'Auth: stores data of Multi Factor Authentication Requests'; --- auth.mfa_backup_codes definition + +-- auth.mfa_backup_codes_ definition CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( user_id uuid NOT NULL, - created_at timestamptz NOT NULL, backup_code VARCHAR(32) NOT NULL, valid BOOLEAN NOT NULL, - time_used timestamptz NOT NULL, + created_at timestamptz NOT NULL, + used_at timestamptz NOT NULL, CONSTRAINT mfa_backup_codes_pkey PRIMARY KEY(user_id, backup_code), CONSTRAINT mfa_backup_codes FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); From aaa3d998783e7c594f7d5c9860a49e39c505d4bf Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 15:11:59 +0800 Subject: [PATCH 004/180] fix: add default --- migrations/20220607041349_add_mfa_schema.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 4dbc0ba1e..e9f85942c 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -31,7 +31,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( comment on table auth.mfa_challenges is 'Auth: stores data of Multi Factor Authentication Requests'; --- auth.mfa_backup_codes_ definition +-- auth.mfa_backup_codes definition CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( user_id uuid NOT NULL, backup_code VARCHAR(32) NOT NULL, @@ -45,4 +45,4 @@ comment on table auth.mfa_backup_codes is 'Auth: stores backup codes for Multi F -- Add MFA toggle on Users table ALTER TABLE auth.users -ADD COLUMN IF NOT EXISTS mfa_enabled boolean NOT NULL; +ADD COLUMN IF NOT EXISTS mfa_enabled boolean NOT NULL DEFAULT FALSE; From c4f81649ebdcbd89d1d967a7e09bcffa4834147b Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 15:16:08 +0800 Subject: [PATCH 005/180] chore: remove whitespace --- migrations/20220607041349_add_mfa_schema.up.sql | 1 - 1 file changed, 1 deletion(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index e9f85942c..ce5efc6e4 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -30,7 +30,6 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( ); comment on table auth.mfa_challenges is 'Auth: stores data of Multi Factor Authentication Requests'; - -- auth.mfa_backup_codes definition CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( user_id uuid NOT NULL, From 0ca674190e7a081bfe517f6b1d7dc35e4b6817bb Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 15:19:48 +0800 Subject: [PATCH 006/180] chore: remove email as a type --- migrations/20220607041349_add_mfa_schema.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index ce5efc6e4..cd1476c4b 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -1,6 +1,6 @@ -- See: https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql/48382296#48382296 DO $$ BEGIN - CREATE TYPE factor_type AS ENUM('phone', 'webauthn', 'email'); + CREATE TYPE factor_type AS ENUM('phone', 'webauthn'); EXCEPTION WHEN duplicate_object THEN null; END $$; From f83ab8ee158d40b68e343f8ec512553f9f23c328 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 15:29:14 +0800 Subject: [PATCH 007/180] fix: prevent updated_at from being nullable --- migrations/20220607041349_add_mfa_schema.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index cd1476c4b..76974eea5 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_factors( factor_type factor_type NOT NULL, enabled BOOLEAN NOT NULL, created_at timestamptz NOT NULL, - updated_at timestamptz NULL, + updated_at timestamptz NOT NULL, secret_key VARCHAR(256) NOT NULL, CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), CONSTRAINT mfa_factors FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE From 04eac1332871248a2303c554dafa939dcb87613e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 14 Jun 2022 15:32:54 +0800 Subject: [PATCH 008/180] chore: update comments --- migrations/20220607041349_add_mfa_schema.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 76974eea5..8451a0b4e 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -18,7 +18,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_factors( CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), CONSTRAINT mfa_factors FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); -comment on table auth.mfa_factors is 'Auth: stores Multi Factor Authentication factor data'; +comment on table auth.mfa_factors is 'Auth: stores metadata about factors'; -- auth.mfa_challenges definition CREATE TABLE IF NOT EXISTS auth.mfa_challenges( @@ -28,7 +28,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id), CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE ); -comment on table auth.mfa_challenges is 'Auth: stores data of Multi Factor Authentication Requests'; +comment on table auth.mfa_challenges is 'Auth: stores metadata about challenge requests made'; -- auth.mfa_backup_codes definition CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( From 66994059642d66893148d6014ea973ecb0113bf8 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 15:21:10 +0800 Subject: [PATCH 009/180] feat:enable and disable mfa --- api/errors.go | 3 ++ api/mfa.go | 64 ++++++++++++++++++++++++++++++++++++ api/mfa_test.go | 50 +++++++++++++++++++++++++++++ models/backup_code.go | 61 +++++++++++++++++++++++++++++++++++ models/factor.go | 75 +++++++++++++++++++++++++++++++++++++++++++ models/factor_test.go | 53 ++++++++++++++++++++++++++++++ models/user.go | 10 ++++++ 7 files changed, 316 insertions(+) create mode 100644 api/mfa.go create mode 100644 api/mfa_test.go create mode 100644 models/backup_code.go create mode 100644 models/factor.go create mode 100644 models/factor_test.go diff --git a/api/errors.go b/api/errors.go index 335dc4e7d..c9c387de4 100644 --- a/api/errors.go +++ b/api/errors.go @@ -17,6 +17,9 @@ var ( DuplicateEmailMsg = "A user with this email address has already been registered" DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") + // MFA Related errors + MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") + MFANotEnabled error = errors.New("MFA can only be disabled when it is enabled") ) var oauthErrorMap = map[int]string{ diff --git a/api/mfa.go b/api/mfa.go new file mode 100644 index 000000000..1a90c0e33 --- /dev/null +++ b/api/mfa.go @@ -0,0 +1,64 @@ +package api + +import ( + "github.com/netlify/gotrue/models" + "github.com/netlify/gotrue/storage" + "net/http" +) + +func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if user.MFAEnabled { + return MFANotEnabled + } + err := a.db.Transaction(func(tx *storage.Connection) error { + if terr := user.EnableMFA(tx); terr != nil { + return terr + } + + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, map[string]interface{}{ + "user_id": user.ID, + "user_email": user.Email, + "user_phone": user.Phone, + }, r.RemoteAddr); terr != nil { + return terr + } + return nil + }) + if err != nil { + return err + } + return sendJSON(w, http.StatusOK, user) + +} + +func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if !user.MFAEnabled { + return MFANotDisabled + } + err := a.db.Transaction(func(tx *storage.Connection) error { + if terr := user.DisableMFA(tx); terr != nil { + return terr + } + + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, map[string]interface{}{ + "user_id": user.ID, + "user_email": user.Email, + "user_phone": user.Phone, + }, r.RemoteAddr); terr != nil { + return terr + } + + return nil + }) + if err != nil { + return err + } + return sendJSON(w, http.StatusOK, user) + +} diff --git a/api/mfa_test.go b/api/mfa_test.go new file mode 100644 index 000000000..1afa87519 --- /dev/null +++ b/api/mfa_test.go @@ -0,0 +1,50 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/models" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type MFATestSuite struct { + suite.Suite + API *API + Config *conf.Configuration + + instanceID uuid.UUID +} + +func TestMFA(t *testing.T) { + api, config, instanceID, err := setupAPIForTestForInstance() + require.NoError(t, err) + + ts := &MFATestSuite{ + API: api, + Config: config, + instanceID: instanceID, + } + defer api.db.Close() + + suite.Run(t, ts) +} + +func (ts *MFATestSuite) SetupTest() { + models.TruncateAll(ts.API.db) + +} + +func (ts *MFATestSuite) TestMFAEnable() { + // Enable MFA for a single user +} + +func (ts *MFATestSuite) TestMFADisable() { + // Disable MFA for a single user +} diff --git a/models/backup_code.go b/models/backup_code.go new file mode 100644 index 000000000..28faff54d --- /dev/null +++ b/models/backup_code.go @@ -0,0 +1,61 @@ +package models + +import ( + "database/sql" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "time" +) + +type BackupCode struct { + UserID uuid.UUID `json:"user_id" db:"user_id"` + CreatedAt *time.Time `json:"created_at" db:"created_at"` + BackupCode string `json:"backup_code" db:"backup_code"` + Valid bool `json:"valid" db:"valid"` + TimeUsed time.Time `json:"time_used" db:"time_used"` +} + +func (BackupCode) TableName() string { + tableName := "backup_codes" + return tableName +} + +// Returns a new backupcode associated with the user +func NewBackupCode(user *User, backupCode string, now *time.Time) (*BackupCode, error) { + bc, err := hashBackupCode(backupCode) + if err != nil { + return nil, err + } + + code := &BackupCode{ + UserID: user.ID, + BackupCode: bc, + CreatedAt: now, + Valid: true, + } + + return code, nil +} + +// FindBackupCodesByUser returns all valid backup codes associated to a user +func FindBackupCodesByUser(tx *storage.Connection, user *User) ([]*BackupCode, error) { + backupCodes := []*BackupCode{} + if err := tx.Q().Where("user_id = ? AND valid = ?", user.ID, true).All(&backupCodes); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return backupCodes, nil + } + return nil, errors.Wrap(err, "Error finding backup codes") + } + return backupCodes, nil +} + +// hashBackupCodes generates a hashed backupCoed from a plaintext string +func hashBackupCode(backupCode string) (string, error) { + bc, err := bcrypt.GenerateFromPassword([]byte(backupCode), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(bc), nil +} diff --git a/models/factor.go b/models/factor.go new file mode 100644 index 000000000..1f7744af6 --- /dev/null +++ b/models/factor.go @@ -0,0 +1,75 @@ +package models + +import ( + "database/sql" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "time" +) + +type Factor struct { + UserID uuid.UUID `json` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Enabled bool `json:"enabled" db:"enabled"` + FactorSimpleName string `json:"factor_simple_name" db:"factor_simple_name"` + SecretKey string `json:'secret_key' db:'secret_key'` + // TODO(Joel): Convert this to an enum + FactorType string `json:"factor_type" db:"factor_type"` +} + +func (Factor) TableName() string { + tableName := "mfa_factors" + return tableName +} + +func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) (*Factor, error) { + // TODO: Pass in secret and hash it using bcrypt or equiv + factor := &Factor{ + ID: id, + UserID: user.ID, + Enabled: true, + FactorSimpleName: factorSimpleName, + SecretKey: secretKey, + FactorType: factorType, + } + return factor, nil +} + +// FindFactorsByUser returns all factors belonging to a user +func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { + factors := []*Factor{} + if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return factors, nil + } + return nil, errors.Wrap(err, "Error finding mfa factors") + } + return factors, nil +} + +// Change the factor simple name +func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { + f.UpdatedAt = time.Now() + return tx.UpdateOnly(f, "factor_simple_name", "updated_at") +} + +func (f *Factor) Disable(tx *storage.Connection) error { + f.Enabled = false + return tx.UpdateOnly(f, "enabled") +} + +func (f *Factor) Enable(tx *storage.Connection) error { + f.Enabled = true + return tx.UpdateOnly(f, "enabled") +} + +// func (f* Factor) FindFactorBySimpleName(tx *storage.Connection) error { + +// } + +// func (f* Factor) FindFactorById(tx *storage.Connection) error { + +// } diff --git a/models/factor_test.go b/models/factor_test.go new file mode 100644 index 000000000..c4ef2d495 --- /dev/null +++ b/models/factor_test.go @@ -0,0 +1,53 @@ +package models + +import ( + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/storage" + "github.com/netlify/gotrue/storage/test" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "testing" +) + +type FactorTestSuite struct { + suite.Suite + db *storage.Connection +} + +func TestFactor(t *testing.T) { + globalConfig, err := conf.LoadGlobal(modelsTestConfig) + require.NoError(t, err) + + conn, err := test.SetupDBConnection(globalConfig) + require.NoError(t, err) + + ts := &FactorTestSuite{ + db: conn, + } + defer ts.db.Close() + + suite.Run(t, ts) +} + +func (ts *FactorTestSuite) SetupTest() { + TruncateAll(ts.db) +} + +func (ts *FactorTestSuite) TestToggleFactorEnabled() { + u, err := NewUser(uuid.Nil, "", "", "", "", nil) + require.NoError(ts.T(), err) + + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "") + require.NoError(ts.T(), err) + + require.NoError(ts.T(), f.Disable(ts.db)) + require.Equal(ts.T(), false, f.Enabled) + + require.NoError(ts.T(), f.Enable(ts.db)) + require.Equal(ts.T(), true, f.Enabled) + + require.NoError(ts.T(), f.Enable(ts.db)) + require.Equal(ts.T(), true, f.Enabled) + +} diff --git a/models/user.go b/models/user.go index 46bba3c4f..1dea6d8e4 100644 --- a/models/user.go +++ b/models/user.go @@ -65,6 +65,7 @@ type User struct { CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` BannedUntil *time.Time `json:"banned_until,omitempty" db:"banned_until"` + MFAEnabled bool `json:"mfa_enabled" db:"mfa_enabled"` } // NewUser initializes a new user from an email, password and user data. @@ -522,3 +523,12 @@ func (u *User) UpdateBannedUntil(tx *storage.Connection) error { return tx.UpdateOnly(u, "banned_until") } +func (u *User) EnableMFA(tx *storage.Connection) error { + u.MFAEnabled = true + return tx.UpdateOnly(u, "mfa_enabled") +} + +func (u *User) DisableMFA(tx *storage.Connection) error { + u.MFAEnabled = false + return tx.UpdateOnly(u, "mfa_enabled") +} From 76c0c6b3e7228df57f660a218727b0e9d4e7f927 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 15:22:33 +0800 Subject: [PATCH 010/180] refactor: remove model files --- models/backup_code.go | 61 ----------------------------------- models/factor.go | 75 ------------------------------------------- models/factor_test.go | 53 ------------------------------ 3 files changed, 189 deletions(-) delete mode 100644 models/backup_code.go delete mode 100644 models/factor.go delete mode 100644 models/factor_test.go diff --git a/models/backup_code.go b/models/backup_code.go deleted file mode 100644 index 28faff54d..000000000 --- a/models/backup_code.go +++ /dev/null @@ -1,61 +0,0 @@ -package models - -import ( - "database/sql" - "github.com/gofrs/uuid" - "github.com/netlify/gotrue/storage" - "github.com/pkg/errors" - "golang.org/x/crypto/bcrypt" - "time" -) - -type BackupCode struct { - UserID uuid.UUID `json:"user_id" db:"user_id"` - CreatedAt *time.Time `json:"created_at" db:"created_at"` - BackupCode string `json:"backup_code" db:"backup_code"` - Valid bool `json:"valid" db:"valid"` - TimeUsed time.Time `json:"time_used" db:"time_used"` -} - -func (BackupCode) TableName() string { - tableName := "backup_codes" - return tableName -} - -// Returns a new backupcode associated with the user -func NewBackupCode(user *User, backupCode string, now *time.Time) (*BackupCode, error) { - bc, err := hashBackupCode(backupCode) - if err != nil { - return nil, err - } - - code := &BackupCode{ - UserID: user.ID, - BackupCode: bc, - CreatedAt: now, - Valid: true, - } - - return code, nil -} - -// FindBackupCodesByUser returns all valid backup codes associated to a user -func FindBackupCodesByUser(tx *storage.Connection, user *User) ([]*BackupCode, error) { - backupCodes := []*BackupCode{} - if err := tx.Q().Where("user_id = ? AND valid = ?", user.ID, true).All(&backupCodes); err != nil { - if errors.Cause(err) == sql.ErrNoRows { - return backupCodes, nil - } - return nil, errors.Wrap(err, "Error finding backup codes") - } - return backupCodes, nil -} - -// hashBackupCodes generates a hashed backupCoed from a plaintext string -func hashBackupCode(backupCode string) (string, error) { - bc, err := bcrypt.GenerateFromPassword([]byte(backupCode), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(bc), nil -} diff --git a/models/factor.go b/models/factor.go deleted file mode 100644 index 1f7744af6..000000000 --- a/models/factor.go +++ /dev/null @@ -1,75 +0,0 @@ -package models - -import ( - "database/sql" - "github.com/gofrs/uuid" - "github.com/netlify/gotrue/storage" - "github.com/pkg/errors" - "time" -) - -type Factor struct { - UserID uuid.UUID `json` - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Enabled bool `json:"enabled" db:"enabled"` - FactorSimpleName string `json:"factor_simple_name" db:"factor_simple_name"` - SecretKey string `json:'secret_key' db:'secret_key'` - // TODO(Joel): Convert this to an enum - FactorType string `json:"factor_type" db:"factor_type"` -} - -func (Factor) TableName() string { - tableName := "mfa_factors" - return tableName -} - -func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) (*Factor, error) { - // TODO: Pass in secret and hash it using bcrypt or equiv - factor := &Factor{ - ID: id, - UserID: user.ID, - Enabled: true, - FactorSimpleName: factorSimpleName, - SecretKey: secretKey, - FactorType: factorType, - } - return factor, nil -} - -// FindFactorsByUser returns all factors belonging to a user -func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { - factors := []*Factor{} - if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { - if errors.Cause(err) == sql.ErrNoRows { - return factors, nil - } - return nil, errors.Wrap(err, "Error finding mfa factors") - } - return factors, nil -} - -// Change the factor simple name -func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { - f.UpdatedAt = time.Now() - return tx.UpdateOnly(f, "factor_simple_name", "updated_at") -} - -func (f *Factor) Disable(tx *storage.Connection) error { - f.Enabled = false - return tx.UpdateOnly(f, "enabled") -} - -func (f *Factor) Enable(tx *storage.Connection) error { - f.Enabled = true - return tx.UpdateOnly(f, "enabled") -} - -// func (f* Factor) FindFactorBySimpleName(tx *storage.Connection) error { - -// } - -// func (f* Factor) FindFactorById(tx *storage.Connection) error { - -// } diff --git a/models/factor_test.go b/models/factor_test.go deleted file mode 100644 index c4ef2d495..000000000 --- a/models/factor_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package models - -import ( - "github.com/gofrs/uuid" - "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/storage" - "github.com/netlify/gotrue/storage/test" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" - "testing" -) - -type FactorTestSuite struct { - suite.Suite - db *storage.Connection -} - -func TestFactor(t *testing.T) { - globalConfig, err := conf.LoadGlobal(modelsTestConfig) - require.NoError(t, err) - - conn, err := test.SetupDBConnection(globalConfig) - require.NoError(t, err) - - ts := &FactorTestSuite{ - db: conn, - } - defer ts.db.Close() - - suite.Run(t, ts) -} - -func (ts *FactorTestSuite) SetupTest() { - TruncateAll(ts.db) -} - -func (ts *FactorTestSuite) TestToggleFactorEnabled() { - u, err := NewUser(uuid.Nil, "", "", "", "", nil) - require.NoError(ts.T(), err) - - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "") - require.NoError(ts.T(), err) - - require.NoError(ts.T(), f.Disable(ts.db)) - require.Equal(ts.T(), false, f.Enabled) - - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) - - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) - -} From 146cb6fbbde80a743f10868372ad480fbec58a4f Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 15:34:37 +0800 Subject: [PATCH 011/180] fix: pull in master --- api/mfa.go | 8 ++++---- api/mfa_test.go | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 1a90c0e33..6ff97cc9b 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -18,11 +18,11 @@ func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { return terr } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, - }, r.RemoteAddr); terr != nil { + }); terr != nil { return terr } return nil @@ -46,11 +46,11 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { return terr } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, - }, r.RemoteAddr); terr != nil { + }); terr != nil { return terr } diff --git a/api/mfa_test.go b/api/mfa_test.go index 1afa87519..aadec131d 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -1,10 +1,6 @@ package api import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" "testing" "github.com/gofrs/uuid" From b76142389887d76818f7d8b6c5d3c16bc5e71008 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 17:22:49 +0800 Subject: [PATCH 012/180] refactor: rename backup codes to recovery codes --- migrations/20220607041349_add_mfa_schema.up.sql | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 8451a0b4e..23ac349f7 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -30,17 +30,18 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( ); comment on table auth.mfa_challenges is 'Auth: stores metadata about challenge requests made'; --- auth.mfa_backup_codes definition -CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes( +-- auth.mfa_recovery_codes definition +CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( + id serial PRIMARY KEY, user_id uuid NOT NULL, - backup_code VARCHAR(32) NOT NULL, + recovery_code VARCHAR(32) NOT NULL, valid BOOLEAN NOT NULL, created_at timestamptz NOT NULL, used_at timestamptz NOT NULL, - CONSTRAINT mfa_backup_codes_pkey PRIMARY KEY(user_id, backup_code), - CONSTRAINT mfa_backup_codes FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE + CONSTRAINT mfa_recovery_codes_user_id_recovery_code_key UNIQUE(user_id, recovery_code), + CONSTRAINT mfa_recovery_codes FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); -comment on table auth.mfa_backup_codes is 'Auth: stores backup codes for Multi Factor Authentication'; +comment on table auth.mfa_recovery_codes is 'Auth: stores recovery codes for Multi Factor Authentication'; -- Add MFA toggle on Users table ALTER TABLE auth.users From 3afbc970115ac0613b6f566dff031cc2a743875d Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 17:41:43 +0800 Subject: [PATCH 013/180] fix: update number of user fields --- api/signup_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/signup_test.go b/api/signup_test.go index aa5137056..e85bd23df 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -78,6 +78,7 @@ func (ts *SignupTestSuite) TestSignup() { } func (ts *SignupTestSuite) TestWebhookTriggered() { + const numUserFields = 11 var callCount int require := ts.Require() assert := ts.Assert() @@ -112,7 +113,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { u, ok := data["user"].(map[string]interface{}) require.True(ok) - assert.Len(u, 10) + assert.Len(u, NUM_USER_FIELDS) // assert.Equal(t, user.ID, u["id"]) TODO assert.Equal("authenticated", u["aud"]) assert.Equal("authenticated", u["role"]) From 040e6d02559956f7e1cdcb0e472bfc77e2a13ec5 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Wed, 15 Jun 2022 17:46:52 +0800 Subject: [PATCH 014/180] Update signup_test.go --- api/signup_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/signup_test.go b/api/signup_test.go index e85bd23df..a5327dece 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -113,7 +113,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { u, ok := data["user"].(map[string]interface{}) require.True(ok) - assert.Len(u, NUM_USER_FIELDS) + assert.Len(u, numUserFields) // assert.Equal(t, user.ID, u["id"]) TODO assert.Equal("authenticated", u["aud"]) assert.Equal("authenticated", u["role"]) From 9cdc5d5a1c55915009ca4dc09ce241532afd5f45 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 19:21:34 +0800 Subject: [PATCH 015/180] test: disable and disable --- api/admin.go | 2 +- api/api.go | 7 +++++++ api/mfa_test.go | 44 +++++++++++++++++++++++++++++++++++++++++--- api/signup_test.go | 2 +- 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/api/admin.go b/api/admin.go index 8c9c620f5..f9117fef5 100644 --- a/api/admin.go +++ b/api/admin.go @@ -175,7 +175,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, diff --git a/api/api.go b/api/api.go index cb98b37fc..884b64a32 100644 --- a/api/api.go +++ b/api/api.go @@ -183,6 +183,13 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Get("/metadata", api.SAMLMetadata) }) + r.Route("/mfa", func(r *router) { + r.Route("/{user_id}", func(r *router) { + r.Use(api.loadUser) + r.Put("/disable_mfa", api.DisableMFA) + r.Put("/enable_mfa", api.EnableMFA) + }) + }) }) if globalConfig.MultiInstanceMode { diff --git a/api/mfa_test.go b/api/mfa_test.go index aadec131d..d9504046c 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -1,7 +1,11 @@ package api import ( + "fmt" + "net/http" + "net/http/httptest" "testing" + "time" "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" @@ -34,13 +38,47 @@ func TestMFA(t *testing.T) { func (ts *MFATestSuite) SetupTest() { models.TruncateAll(ts.API.db) - + u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model") + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") } func (ts *MFATestSuite) TestMFAEnable() { - // Enable MFA for a single user + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), u.EnableMFA(ts.API.db)) + require.NoError(ts.T(), u.DisableMFA(ts.API.db)) + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/enable_mfa", u.ID), nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), w.Code, http.StatusOK) + + u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + require.True(ts.T(), u.MFAEnabled) + } func (ts *MFATestSuite) TestMFADisable() { - // Disable MFA for a single user + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), u.EnableMFA(ts.API.db)) + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/disable_mfa", u.ID), nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), w.Code, http.StatusOK) + + u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + require.False(ts.T(), u.MFAEnabled) } diff --git a/api/signup_test.go b/api/signup_test.go index e85bd23df..a5327dece 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -113,7 +113,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { u, ok := data["user"].(map[string]interface{}) require.True(ok) - assert.Len(u, NUM_USER_FIELDS) + assert.Len(u, numUserFields) // assert.Equal(t, user.ID, u["id"]) TODO assert.Equal("authenticated", u["aud"]) assert.Equal("authenticated", u["role"]) From ce8f5b3ac3488d3b9eae08314f6c4dbe4954b61f Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 15 Jun 2022 20:05:44 +0800 Subject: [PATCH 016/180] feat: add recovery codes api --- api/api.go | 1 + api/errors.go | 5 ++-- api/mfa.go | 49 +++++++++++++++++++++++++++++++ api/mfa_test.go | 33 +++++++++++++++++++++ crypto/crypto.go | 8 +++-- models/audit_log_entry.go | 8 +++-- models/recovery_code.go | 61 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 7 deletions(-) create mode 100644 models/recovery_code.go diff --git a/api/api.go b/api/api.go index 884b64a32..ec9897450 100644 --- a/api/api.go +++ b/api/api.go @@ -188,6 +188,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadUser) r.Put("/disable_mfa", api.DisableMFA) r.Put("/enable_mfa", api.EnableMFA) + r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) }) }) }) diff --git a/api/errors.go b/api/errors.go index c9c387de4..2a701df5e 100644 --- a/api/errors.go +++ b/api/errors.go @@ -18,8 +18,9 @@ var ( DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") // MFA Related errors - MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") - MFANotEnabled error = errors.New("MFA can only be disabled when it is enabled") + MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") + MFANotEnabled error = errors.New("MFA can only be disabled when it is Enabled") + MFANotEnabledError error = errors.New("MFA not enabled") ) var oauthErrorMap = map[int]string{ diff --git a/api/mfa.go b/api/mfa.go index 6ff97cc9b..1b2694ea3 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -1,11 +1,18 @@ package api import ( + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" "net/http" + "time" ) +// RecoveryCodesResponse repreesnts a successful Backup code generation response +type RecoveryCodesResponse struct { + RecoveryCodes []string +} + func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) @@ -62,3 +69,45 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, user) } + +func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { + const NUM_RECOVERY_CODES = 8 + const RECOVERY_CODE_LENGTH = 8 + + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if !user.MFAEnabled { + return MFANotEnabledError + } + now := time.Now() + recoveryCodeModels := []*models.RecoveryCode{} + var terr error + var recoveryCode string + var recoveryCodes []string + var recoveryCodeModel *models.RecoveryCode + + for i := 0; i < NUM_RECOVERY_CODES; i++ { + recoveryCode = crypto.SecureToken(RECOVERY_CODE_LENGTH) + recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode, &now) + if terr != nil { + return internalServerError("Error creating backup code").WithInternalError(terr) + } + recoveryCodes = append(recoveryCodes, recoveryCode) + recoveryCodeModels = append(recoveryCodeModels, recoveryCodeModel) + } + terr = a.db.Transaction(func(tx *storage.Connection) error { + if terr = tx.Create(recoveryCodeModels); terr != nil { + return terr + } + + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.GenerateRecoveryCodesAction, r.RemoteAddr, nil); terr != nil { + return terr + } + return nil + }) + + return sendJSON(w, http.StatusOK, &RecoveryCodesResponse{ + RecoveryCodes: recoveryCodes, + }) +} diff --git a/api/mfa_test.go b/api/mfa_test.go index d9504046c..b602002fe 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -82,3 +83,35 @@ func (ts *MFATestSuite) TestMFADisable() { require.NoError(ts.T(), err) require.False(ts.T(), u.MFAEnabled) } + +func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { + const EXPECTED_NUM_OF_RECOVERY_CODES = 8 + + u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, nil) + u.MFAEnabled = true + + err = ts.API.db.Create(u) + require.NoError(ts.T(), err) + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test1@example.com", ts.Config.JWT.Aud) + ts.Require().NoError(err) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/mfa/%s/generate_recovery_codes", user.ID), nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + ts.API.handler.ServeHTTP(w, req) + + data := make(map[string]interface{}) + + require.Equal(ts.T(), http.StatusOK, w.Code) + + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + backupCodes := data["RecoveryCodes"].([]interface{}) + + numCodes := len(backupCodes) + require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, numCodes) +} diff --git a/crypto/crypto.go b/crypto/crypto.go index 22df538a1..9bb01ef6d 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -13,8 +13,12 @@ import ( ) // SecureToken creates a new random token -func SecureToken() string { - b := make([]byte, 16) +func SecureToken(options ...int) string { + length := 16 + if len(options) > 0 { + length = options[0] + } + b := make([]byte, length) if _, err := io.ReadFull(rand.Reader, b); err != nil { panic(err.Error()) // rand should never fail } diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index ffa942a76..2bbadc83d 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -28,6 +28,7 @@ const ( UserRepeatedSignUpAction AuditAction = "user_repeated_signup" TokenRevokedAction AuditAction = "token_revoked" TokenRefreshedAction AuditAction = "token_refreshed" + GenerateRecoveryCodesAction AuditAction = "generate_recovery_codes" account auditLogType = "account" team auditLogType = "team" @@ -48,15 +49,16 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ UserRecoveryRequestedAction: user, UserConfirmationRequestedAction: user, UserRepeatedSignUpAction: user, + GenerateRecoveryCodesAction: user, } // AuditLogEntry is the database model for audit log entries. type AuditLogEntry struct { InstanceID uuid.UUID `json:"-" db:"instance_id"` ID uuid.UUID `json:"id" db:"id"` - Payload JSONMap `json:"payload" db:"payload"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - IPAddress string `json:"ip_address" db:"ip_address"` + Payload JSONMap `json:"payload" db:"payload"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + IPAddress string `json:"ip_address" db:"ip_address"` } func (AuditLogEntry) TableName() string { diff --git a/models/recovery_code.go b/models/recovery_code.go new file mode 100644 index 000000000..7cc1cadc2 --- /dev/null +++ b/models/recovery_code.go @@ -0,0 +1,61 @@ +package models + +import ( + "database/sql" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "time" +) + +type RecoveryCode struct { + UserID uuid.UUID `json:"user_id" db:"user_id"` + CreatedAt *time.Time `json:"created_at" db:"created_at"` + RecoveryCode string `json:"recovery_code" db:"recovery_code"` + Valid bool `json:"valid" db:"valid"` + TimeUsed time.Time `json:"time_used" db:"time_used"` +} + +func (RecoveryCode) TableName() string { + tableName := "recovery_codes" + return tableName +} + +// Returns a new recovery code associated with the user +func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*RecoveryCode, error) { + rc, err := hashRecoveryCode(recoveryCode) + if err != nil { + return nil, err + } + + code := &RecoveryCode{ + UserID: user.ID, + RecoveryCode: rc, + CreatedAt: now, + Valid: true, + } + + return code, nil +} + +// FindRecoveryCodes returns all valid recovery codes associated to a user +func FindRecoveryCodesByUser(tx *storage.Connection, user *User) ([]*RecoveryCode, error) { + recoveryCodes := []*RecoveryCode{} + if err := tx.Q().Where("user_id = ? AND valid = ?", user.ID, true).All(&recoveryCodes); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return recoveryCodes, nil + } + return nil, errors.Wrap(err, "Error finding recovery codes") + } + return recoveryCodes, nil +} + +// hashRecoveryCode generates a hashed recoveryCode from a plaintext string +func hashRecoveryCode(recoveryCode string) (string, error) { + rc, err := bcrypt.GenerateFromPassword([]byte(recoveryCode), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(rc), nil +} From 6349e8f8fc84cfb431c3d5ddf7edded0e6ecf747 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 14:49:25 +0800 Subject: [PATCH 017/180] feat: add initial modles for factor --- models/audit_log_entry.go | 2 ++ models/factor.go | 66 +++++++++++++++++++++++++++++++++++++++ models/factor_test.go | 53 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 models/factor.go create mode 100644 models/factor_test.go diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 2bbadc83d..18c354357 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -29,6 +29,7 @@ const ( TokenRevokedAction AuditAction = "token_revoked" TokenRefreshedAction AuditAction = "token_refreshed" GenerateRecoveryCodesAction AuditAction = "generate_recovery_codes" + EnrollFactorAction AuditAction = "factor_enrolled" account auditLogType = "account" team auditLogType = "team" @@ -50,6 +51,7 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ UserConfirmationRequestedAction: user, UserRepeatedSignUpAction: user, GenerateRecoveryCodesAction: user, + EnrollFactorAction: user, } // AuditLogEntry is the database model for audit log entries. diff --git a/models/factor.go b/models/factor.go new file mode 100644 index 000000000..41006dee2 --- /dev/null +++ b/models/factor.go @@ -0,0 +1,66 @@ +package models + +import ( + "database/sql" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "time" +) + +type Factor struct { + UserID uuid.UUID `json` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Enabled bool `json:"enabled" db:"enabled"` + FactorSimpleName string `json:"factor_simple_name" db:"factor_simple_name"` + SecretKey string `json:'-' db:'secret_key'` + FactorType string `json:"factor_type" db:"factor_type"` +} + +func (Factor) TableName() string { + tableName := "mfa_factors" + return tableName +} + +func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) (*Factor, error) { + // TODO: Pass in secret and hash it using bcrypt or equiv + factor := &Factor{ + ID: id, + UserID: user.ID, + Enabled: true, + FactorSimpleName: factorSimpleName, + SecretKey: secretKey, + FactorType: factorType, + } + return factor, nil +} + +// FindFactorsByUser returns all factors belonging to a user +func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { + factors := []*Factor{} + if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return factors, nil + } + return nil, errors.Wrap(err, "Error finding mfa factors") + } + return factors, nil +} + +// Change the factor simple name +func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { + f.UpdatedAt = time.Now() + return tx.UpdateOnly(f, "factor_simple_name", "updated_at") +} + +func (f *Factor) Disable(tx *storage.Connection) error { + f.Enabled = false + return tx.UpdateOnly(f, "enabled") +} + +func (f *Factor) Enable(tx *storage.Connection) error { + f.Enabled = true + return tx.UpdateOnly(f, "enabled") +} diff --git a/models/factor_test.go b/models/factor_test.go new file mode 100644 index 000000000..c4ef2d495 --- /dev/null +++ b/models/factor_test.go @@ -0,0 +1,53 @@ +package models + +import ( + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/storage" + "github.com/netlify/gotrue/storage/test" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "testing" +) + +type FactorTestSuite struct { + suite.Suite + db *storage.Connection +} + +func TestFactor(t *testing.T) { + globalConfig, err := conf.LoadGlobal(modelsTestConfig) + require.NoError(t, err) + + conn, err := test.SetupDBConnection(globalConfig) + require.NoError(t, err) + + ts := &FactorTestSuite{ + db: conn, + } + defer ts.db.Close() + + suite.Run(t, ts) +} + +func (ts *FactorTestSuite) SetupTest() { + TruncateAll(ts.db) +} + +func (ts *FactorTestSuite) TestToggleFactorEnabled() { + u, err := NewUser(uuid.Nil, "", "", "", "", nil) + require.NoError(ts.T(), err) + + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "") + require.NoError(ts.T(), err) + + require.NoError(ts.T(), f.Disable(ts.db)) + require.Equal(ts.T(), false, f.Enabled) + + require.NoError(ts.T(), f.Enable(ts.db)) + require.Equal(ts.T(), true, f.Enabled) + + require.NoError(ts.T(), f.Enable(ts.db)) + require.Equal(ts.T(), true, f.Enabled) + +} From 4f5895ee1ec286313765e6b8d1c5d56b0915e714 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 15:31:36 +0800 Subject: [PATCH 018/180] feat: initial enroll endpoint --- api/api.go | 1 + api/mfa.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 +++ 4 files changed, 98 insertions(+) diff --git a/api/api.go b/api/api.go index ec9897450..85b3d9e8b 100644 --- a/api/api.go +++ b/api/api.go @@ -189,6 +189,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Put("/disable_mfa", api.DisableMFA) r.Put("/enable_mfa", api.EnableMFA) r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) + r.Post("/enroll_factor", api.EnrollFactor) }) }) }) diff --git a/api/mfa.go b/api/mfa.go index 1b2694ea3..0b9ceefd6 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -1,13 +1,38 @@ package api import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" + "github.com/pquerna/otp/totp" + "image/png" "net/http" "time" ) +type EnrollFactorParams struct { + FactorSimpleName string `json:"factor_simple_name"` + FactorType string `json:"factor_type"` + Issuer string `json:"issuer"` +} + +type TOTPObject struct { + QRCode string + Secret string + URI string +} + +type EnrollFactorResponse struct { + ID string + CreatedAt string + Type string + TOTP TOTPObject +} + // RecoveryCodesResponse repreesnts a successful Backup code generation response type RecoveryCodesResponse struct { RecoveryCodes []string @@ -111,3 +136,70 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro RecoveryCodes: recoveryCodes, }) } + +func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { + const FACTOR_PREFIX = "factor" + const IMAGE_SIDE_LENGTH = 300 + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if !user.MFAEnabled { + return MFANotEnabledError + } + + params := &EnrollFactorParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Could not read EnrollFactor params: %v", err) + } + + if (params.FactorType != "totp") && (params.FactorType != "webauthn") { + return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") + } + + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: params.Issuer, + AccountName: params.Issuer, + }) + + if err != nil { + return internalServerError("Error generating QR Code secret key").WithInternalError(err) + } + var buf bytes.Buffer + + // Test with QRCode Encode + img, err := key.Image(IMAGE_SIDE_LENGTH, IMAGE_SIDE_LENGTH) + png.Encode(&buf, img) + if err != nil { + return internalServerError("Error generating QR Code image").WithInternalError(err) + } + qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) + factorID := fmt.Sprintf("%s_%s", FACTOR_PREFIX, crypto.SecureToken()) + + factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, key.Secret()) + if terr != nil { + return internalServerError("Database error creating factor").WithInternalError(err) + } + + terr = a.db.Transaction(func(tx *storage.Connection) error { + if terr = tx.Create(factor); terr != nil { + return terr + } + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.EnrollFactorAction, r.RemoteAddr, nil); terr != nil { + return terr + } + + return nil + }) + + return sendJSON(w, http.StatusOK, &EnrollFactorResponse{ + ID: factor.ID, + Type: factor.FactorType, + TOTP: TOTPObject{ + QRCode: fmt.Sprintf("data:img/png;base64,%v", qrAsBase64), + Secret: factor.SecretKey, + URI: key.URL(), + }, + }) +} diff --git a/go.mod b/go.mod index 2746cd138..137371f5c 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/opentracing/opentracing-go v1.1.0 github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pkg/errors v0.9.1 + github.com/pquerna/otp v1.3.0 // indirect github.com/rs/cors v1.6.0 github.com/russellhaering/gosaml2 v0.6.1-0.20210916051624-757d23f1bc28 github.com/russellhaering/goxmldsig v1.1.1 diff --git a/go.sum b/go.sum index e059f2cb7..4f756ff17 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= +github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -463,6 +465,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs= +github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= From 0fa975295094c38d408243b5f298b13072bbdd19 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 16:55:28 +0800 Subject: [PATCH 019/180] feat: initial commit for challenge API --- api/api.go | 1 + api/mfa.go | 73 +++++++++++++++++++++++++++++++++++++++ models/audit_log_entry.go | 2 ++ models/challenge.go | 28 +++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 models/challenge.go diff --git a/api/api.go b/api/api.go index 85b3d9e8b..242d82836 100644 --- a/api/api.go +++ b/api/api.go @@ -190,6 +190,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Put("/enable_mfa", api.EnableMFA) r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) r.Post("/enroll_factor", api.EnrollFactor) + r.Post("/challenge_factor", api.ChallengeFactor) }) }) }) diff --git a/api/mfa.go b/api/mfa.go index 0b9ceefd6..058fe7ff2 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -33,6 +33,19 @@ type EnrollFactorResponse struct { TOTP TOTPObject } +type ChallengeFactorParams struct { + FactorID string + FactorSimpleName string +} + +type ChallengeFactorResponse struct { + ChallengeID string + CreatedAt string + UpdatedAt string + ExpiredAt string + FactorID string +} + // RecoveryCodesResponse repreesnts a successful Backup code generation response type RecoveryCodesResponse struct { RecoveryCodes []string @@ -203,3 +216,63 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { }, }) } + +func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { + const CHALLENGE_PREFIX = "challenge" + if params.FactorID != "" && params.FactorSimpleName != "" { + return unprocessableEntityError("Only an email address or phone number should be provided on signup.") + } + if params.FactorID != "" { + + // Handle finding logic here + } else if params.FactorSimpleName != "" { + // Handle finding logic here + } + + // Filter between finding by EITHER factor simple name OR by ID. Error if both are not present + // Insert corresponding FindBy Clauses (e.g. models.FindBySimpleNameAndUser and models.FindByUserAndId) + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if !user.MFAEnabled { + return MFANotEnabledError + } + + params := &ChallengeFactorParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Could not read EnrollFactor params: %v", err) + } + + challenge, terr := models.NewChallenge(factor) + if terr != nil { + return internalServerError("Database error creating challenge").WithInternalError(err) + } + + terr = a.db.Transaction(func(tx *storage.Connection) error { + if terr = tx.Create(challenge); terr != nil { + return terr + } + // TODO: store data about what was challenged perhaps + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ + "factor_id": params.FactorID, + "factor_simple_name": params.FactorSimpleName, + }); terr != nil { + return terr + } + + return nil + }) + // Notes: If you make 5 consecutive Challenges all 5 will be valid until expiry + // Should we have an easy way to cancel a challenge? + + // Create these details + return sendJSON(w, http.StatusOK, *ChallengeFactorResponse{ + // ID: + // CreatedAt: + // UpdatedAt: + // ExpiresAt: + // FactorID: factor.ID + }) +} diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 18c354357..7e98406bf 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -30,6 +30,7 @@ const ( TokenRefreshedAction AuditAction = "token_refreshed" GenerateRecoveryCodesAction AuditAction = "generate_recovery_codes" EnrollFactorAction AuditAction = "factor_enrolled" + CreateChallengeAction AuditAction = "challenge_created" account auditLogType = "account" team auditLogType = "team" @@ -52,6 +53,7 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ UserRepeatedSignUpAction: user, GenerateRecoveryCodesAction: user, EnrollFactorAction: user, + CreateChallengeAction: user, } // AuditLogEntry is the database model for audit log entries. diff --git a/models/challenge.go b/models/challenge.go new file mode 100644 index 000000000..86c293a63 --- /dev/null +++ b/models/challenge.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "github.com/netlify/gotrue/storage" +) + +type Challenge struct { + ID string `json:"challenge_id" db:"id"` + FactorID string `json:"factor_id" db:"factor_id"` + CreatedAt *time.Time `json:"created_at" db:"created_at"` +} + +func (Challenge) TableName() string { + tableName := "mfa_challenges" +} + +func NewChallenge(factor *Factor) (*Challenge, error) { + challenge := &Challenge{ + ID: id, + FactorID: factor.ID, + } +} + +func FindChallengeByFactor(tx *storage.Connection, factor *Factor) ([]*Challenge, error) { + challenge := []*Challenge{} + +} From 680941ae88a60b4e750be10dd32dfaaec1f2894b Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 16:59:30 +0800 Subject: [PATCH 020/180] refactor: add logic for filtering between factorid and simple name --- api/mfa.go | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 058fe7ff2..c4409c9a7 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -218,19 +218,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { - const CHALLENGE_PREFIX = "challenge" - if params.FactorID != "" && params.FactorSimpleName != "" { - return unprocessableEntityError("Only an email address or phone number should be provided on signup.") - } - if params.FactorID != "" { - - // Handle finding logic here - } else if params.FactorSimpleName != "" { - // Handle finding logic here - } - - // Filter between finding by EITHER factor simple name OR by ID. Error if both are not present - // Insert corresponding FindBy Clauses (e.g. models.FindBySimpleNameAndUser and models.FindByUserAndId) ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -245,6 +232,19 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read EnrollFactor params: %v", err) } + const CHALLENGE_PREFIX = "challenge" + if params.FactorID != "" && params.FactorSimpleName != "" { + return unprocessableEntityError("Only a FactorID or FactorSimpleName should be provided on signup.") + } + if params.FactorID != "" { + // models.FindByFactorIDAndUser + + // Handle finding logic here + } else if params.FactorSimpleName != "" { + // Handle finding logic here + // models.FindBySimpleNameAndUser + } + challenge, terr := models.NewChallenge(factor) if terr != nil { return internalServerError("Database error creating challenge").WithInternalError(err) @@ -264,8 +264,6 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return nil }) - // Notes: If you make 5 consecutive Challenges all 5 will be valid until expiry - // Should we have an easy way to cancel a challenge? // Create these details return sendJSON(w, http.StatusOK, *ChallengeFactorResponse{ From 53c089d594f6d06137c917e63c24a252b16d3b39 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 17:19:10 +0800 Subject: [PATCH 021/180] refactor: split based on factor simple name or id --- api/mfa.go | 19 +++++++++---------- models/challenge.go | 6 ++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index c4409c9a7..22078f259 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -218,6 +218,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { + const challengeExpiryDuration = 300 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -232,17 +233,16 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read EnrollFactor params: %v", err) } - const CHALLENGE_PREFIX = "challenge" if params.FactorID != "" && params.FactorSimpleName != "" { return unprocessableEntityError("Only a FactorID or FactorSimpleName should be provided on signup.") } + /// var factor if params.FactorID != "" { - // models.FindByFactorIDAndUser + // factor = models.FindByFactorID() - // Handle finding logic here } else if params.FactorSimpleName != "" { - // Handle finding logic here - // models.FindBySimpleNameAndUser + // factor = models.FindFactorBySimpleName() + // If errors } challenge, terr := models.NewChallenge(factor) @@ -254,7 +254,6 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { if terr = tx.Create(challenge); terr != nil { return terr } - // TODO: store data about what was challenged perhaps if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ "factor_id": params.FactorID, "factor_simple_name": params.FactorSimpleName, @@ -267,10 +266,10 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { // Create these details return sendJSON(w, http.StatusOK, *ChallengeFactorResponse{ - // ID: - // CreatedAt: - // UpdatedAt: - // ExpiresAt: + // ID: challenge.ID + // CreatedAt: challenge.CreatedAt + // Error handle the manipulation below + // ExpiresAt: time.Parse(challenge.CreatedAt).Add(time.Second * challengeExpiryDuration) // FactorID: factor.ID }) } diff --git a/models/challenge.go b/models/challenge.go index 86c293a63..a8f0927d2 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -1,4 +1,4 @@ -package main +package api import ( "fmt" @@ -15,9 +15,11 @@ func (Challenge) TableName() string { tableName := "mfa_challenges" } +const CHALLENGE_PREFIX = "challenge" + func NewChallenge(factor *Factor) (*Challenge, error) { challenge := &Challenge{ - ID: id, + ID: fmt.Sprintf("%s_%s", CHALLENGE_PREFIX, crypto.SecureToken()), FactorID: factor.ID, } } From 9c9516bc3c9ed605779faaf2027b6d04c70c8062 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 17:57:39 +0800 Subject: [PATCH 022/180] refactor: make endpoints idempotent --- api/errors.go | 3 --- api/mfa.go | 6 ------ api/mfa_test.go | 5 ++--- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/api/errors.go b/api/errors.go index c9c387de4..335dc4e7d 100644 --- a/api/errors.go +++ b/api/errors.go @@ -17,9 +17,6 @@ var ( DuplicateEmailMsg = "A user with this email address has already been registered" DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") - // MFA Related errors - MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") - MFANotEnabled error = errors.New("MFA can only be disabled when it is enabled") ) var oauthErrorMap = map[int]string{ diff --git a/api/mfa.go b/api/mfa.go index 6ff97cc9b..ef77b187e 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -10,9 +10,6 @@ func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - if user.MFAEnabled { - return MFANotEnabled - } err := a.db.Transaction(func(tx *storage.Connection) error { if terr := user.EnableMFA(tx); terr != nil { return terr @@ -38,9 +35,6 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return MFANotDisabled - } err := a.db.Transaction(func(tx *storage.Connection) error { if terr := user.DisableMFA(tx); terr != nil { return terr diff --git a/api/mfa_test.go b/api/mfa_test.go index d9504046c..706847136 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -38,6 +38,8 @@ func TestMFA(t *testing.T) { func (ts *MFATestSuite) SetupTest() { models.TruncateAll(ts.API.db) + + // Create user u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") @@ -45,9 +47,6 @@ func (ts *MFATestSuite) SetupTest() { func (ts *MFATestSuite) TestMFAEnable() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), u.EnableMFA(ts.API.db)) - require.NoError(ts.T(), u.DisableMFA(ts.API.db)) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) From f4e7a2f731754c5bf7c13ad710e49c107179a4c7 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 18:01:09 +0800 Subject: [PATCH 023/180] chore: undo whitespace change --- api/admin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/admin.go b/api/admin.go index f9117fef5..8c9c620f5 100644 --- a/api/admin.go +++ b/api/admin.go @@ -175,7 +175,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, From cb6a175e102ba5cf78639ecf36632bfc3d8c267e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 18:09:48 +0800 Subject: [PATCH 024/180] chore: remove whitespace --- api/mfa.go | 5 ----- api/mfa_test.go | 9 ++------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index ef77b187e..989fb53d8 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -14,7 +14,6 @@ func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { if terr := user.EnableMFA(tx); terr != nil { return terr } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, @@ -28,7 +27,6 @@ func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { return err } return sendJSON(w, http.StatusOK, user) - } func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { @@ -39,7 +37,6 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { if terr := user.DisableMFA(tx); terr != nil { return terr } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, @@ -47,12 +44,10 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { }); terr != nil { return terr } - return nil }) if err != nil { return err } return sendJSON(w, http.StatusOK, user) - } diff --git a/api/mfa_test.go b/api/mfa_test.go index 706847136..bc84ac7ba 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -16,9 +16,8 @@ import ( type MFATestSuite struct { suite.Suite - API *API - Config *conf.Configuration - + API *API + Config *conf.Configuration instanceID uuid.UUID } @@ -52,7 +51,6 @@ func (ts *MFATestSuite) TestMFAEnable() { req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/enable_mfa", u.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), w.Code, http.StatusOK) @@ -60,19 +58,16 @@ func (ts *MFATestSuite) TestMFAEnable() { u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) require.True(ts.T(), u.MFAEnabled) - } func (ts *MFATestSuite) TestMFADisable() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), u.EnableMFA(ts.API.db)) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/disable_mfa", u.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), w.Code, http.StatusOK) From 4c7ae3f76023ab834afad0099e8885b1a9c7b970 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 18:27:57 +0800 Subject: [PATCH 025/180] chore: naming and initial tests --- api/errors.go | 2 -- api/mfa.go | 4 ++-- api/mfa_test.go | 11 +++------- models/recovery_code.go | 4 ++-- models/recovery_code_test.go | 39 ++++++++++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 models/recovery_code_test.go diff --git a/api/errors.go b/api/errors.go index 2a701df5e..f287e9491 100644 --- a/api/errors.go +++ b/api/errors.go @@ -18,8 +18,6 @@ var ( DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") // MFA Related errors - MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") - MFANotEnabled error = errors.New("MFA can only be disabled when it is Enabled") MFANotEnabledError error = errors.New("MFA not enabled") ) diff --git a/api/mfa.go b/api/mfa.go index 1b2694ea3..934efe217 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -8,7 +8,7 @@ import ( "time" ) -// RecoveryCodesResponse repreesnts a successful Backup code generation response +// RecoveryCodesResponse repreesnts a successful recovery code generation response type RecoveryCodesResponse struct { RecoveryCodes []string } @@ -91,7 +91,7 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro recoveryCode = crypto.SecureToken(RECOVERY_CODE_LENGTH) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode, &now) if terr != nil { - return internalServerError("Error creating backup code").WithInternalError(terr) + return internalServerError("Error creating recovery code").WithInternalError(terr) } recoveryCodes = append(recoveryCodes, recoveryCode) recoveryCodeModels = append(recoveryCodeModels, recoveryCodeModel) diff --git a/api/mfa_test.go b/api/mfa_test.go index b602002fe..57a3e30b8 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -74,7 +74,6 @@ func (ts *MFATestSuite) TestMFADisable() { req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/disable_mfa", u.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), w.Code, http.StatusOK) @@ -88,11 +87,10 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { const EXPECTED_NUM_OF_RECOVERY_CODES = 8 u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, nil) - u.MFAEnabled = true + u.EnableMFA() err = ts.API.db.Create(u) require.NoError(ts.T(), err) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) @@ -102,16 +100,13 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/mfa/%s/generate_recovery_codes", user.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - ts.API.handler.ServeHTTP(w, req) data := make(map[string]interface{}) - require.Equal(ts.T(), http.StatusOK, w.Code) - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - backupCodes := data["RecoveryCodes"].([]interface{}) - numCodes := len(backupCodes) + recoveryCodes := data["RecoveryCodes"].([]interface{}) + numCodes := len(recoveryCodes) require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, numCodes) } diff --git a/models/recovery_code.go b/models/recovery_code.go index 7cc1cadc2..e3927f372 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -39,8 +39,8 @@ func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*Recovery return code, nil } -// FindRecoveryCodes returns all valid recovery codes associated to a user -func FindRecoveryCodesByUser(tx *storage.Connection, user *User) ([]*RecoveryCode, error) { +// FindValidRecoveryCodes returns all valid recovery codes associated to a user +func FindValidRecoveryCodesByUser(tx *storage.Connection, user *User) ([]*RecoveryCode, error) { recoveryCodes := []*RecoveryCode{} if err := tx.Q().Where("user_id = ? AND valid = ?", user.ID, true).All(&recoveryCodes); err != nil { if errors.Cause(err) == sql.ErrNoRows { diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go new file mode 100644 index 000000000..24c546878 --- /dev/null +++ b/models/recovery_code_test.go @@ -0,0 +1,39 @@ +package api + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/storage" + "github.com/netlify/gotrue/storage/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + + +type RecoveryCodeTestSuite struct { + suite.Suite + db *storage.Connection +} + +func (ts *RecoveryCodeTestSuite) SetupTest() { + TruncateAll(ts.db) +} + + +func TestRecoveryCode(t *testing.T) { + globalConfig, err := conf.LoadGlobal(modelsTestConfig) + require.NoError(t, err) + + conn, err := test.SetupDBConnection(globalConfig) + require.NoError(t, err) + + ts := &UserTestSuite{ + db: conn, + } + defer ts.db.Close() + + suite.Run(t, ts) +} From c7bdbc440b28b9910ce3f5d60236caec8bd1bf92 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 16 Jun 2022 18:31:49 +0800 Subject: [PATCH 026/180] test: add model test for good measure --- models/user_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/models/user_test.go b/models/user_test.go index 28771df4f..a24066e14 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -38,6 +38,18 @@ func TestUser(t *testing.T) { suite.Run(t, ts) } +func (ts *UserTestSuite) TestToggleMFA() { + u, err := NewUser(uuid.Nil, "", "", "", "", nil) + require.NoError(ts.T(), err) + require.False(ts.T(), u.MFAEnabled) + + require.NoError(ts.T(), u.EnableMFA(ts.db)) + require.True(ts.T(), u.MFAEnabled) + + require.NoError(ts.T(), u.DisableMFA(ts.db)) + require.False(ts.T(), u.MFAEnabled) +} + func (ts *UserTestSuite) TestUpdateAppMetadata() { u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) From fef5980fa9f78dfca59b16c1fcfa399500b09d78 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 17 Jun 2022 11:35:43 +0800 Subject: [PATCH 027/180] feat: add find factor methods --- api/mfa.go | 36 +++++++++++++++++++++++------------- models/errors.go | 7 +++++++ models/factor.go | 30 +++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 22078f259..e0f69317b 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -218,7 +218,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { - const challengeExpiryDuration = 300 + const CHALLENGE_EXPIRY_DURATION = 300 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -232,17 +232,24 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { if err != nil { return badRequestError("Could not read EnrollFactor params: %v", err) } + factorID := params.FactorID + factorSimpleName := params.FactorSimpleName - if params.FactorID != "" && params.FactorSimpleName != "" { + if factorID != "" && factorSimpleName != "" { return unprocessableEntityError("Only a FactorID or FactorSimpleName should be provided on signup.") } - /// var factor - if params.FactorID != "" { - // factor = models.FindByFactorID() + + if factorID != "" { + factor, err := models.FindByFactorID(factorID) } else if params.FactorSimpleName != "" { - // factor = models.FindFactorBySimpleName() - // If errors + factor, err := models.FindFactorBySimpleName(factorSimpleName) + } + if err != nil { + if models.IsNotFoundError(err) { + return notFoundError(err.Error()) + } + return internalServerError("Database error finding factor").WithInternalError(err) } challenge, terr := models.NewChallenge(factor) @@ -263,13 +270,16 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return nil }) + creationTime := challenge.CreatedAt + expiryTimeAsTimestamp, err := time.Parse(time.RFC3339, creationTime) + if err != nil { + return internalServerError("Error parsing database timestamp").WithInternalError(err) + } - // Create these details return sendJSON(w, http.StatusOK, *ChallengeFactorResponse{ - // ID: challenge.ID - // CreatedAt: challenge.CreatedAt - // Error handle the manipulation below - // ExpiresAt: time.Parse(challenge.CreatedAt).Add(time.Second * challengeExpiryDuration) - // FactorID: factor.ID + ID: challenge.ID, + CreatedAt: creationTime, + ExpiresAt: expiryTimeAsTimestamp.Add(time.Second * CHALLENGE_EXPIRY_DURATION), + FactorID: factor.ID, }) } diff --git a/models/errors.go b/models/errors.go index 6c33c70c2..054d8fa00 100644 --- a/models/errors.go +++ b/models/errors.go @@ -54,6 +54,13 @@ func (e InstanceNotFoundError) Error() string { return "Instance not found" } +// FactorNotFoundError represents when a user is not found. +type FactorNotFoundError struct{} + +func (e FactorNotFoundError) Error() string { + return "Factor not found" +} + type TotpSecretNotFoundError struct{} func (e TotpSecretNotFoundError) Error() string { diff --git a/models/factor.go b/models/factor.go index 41006dee2..04d6e28b4 100644 --- a/models/factor.go +++ b/models/factor.go @@ -38,7 +38,7 @@ func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) ( } // FindFactorsByUser returns all factors belonging to a user -func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { +func FindFactorsByUser(tx *storage.Connection, user *User) (*Factor, error) { factors := []*Factor{} if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { if errors.Cause(err) == sql.ErrNoRows { @@ -49,6 +49,22 @@ func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { return factors, nil } +func (f *Factor) FindFactorByFactorID(tx *storage.Connection, factorID string) (*Factor, error) { + factor, err := findFactor(tx, "id = ?", factorID) + if err != nil { + return nil, FactorNotFoundError{} + } + return factor, nil +} + +func (f *Factor) FindFactorBySimpleName(tx *storage.Connection, simpleName string) ([]*Factor, error) { + factor, err := findFactor(tx, "factor_simple_name = ?", simpleName) + if err != nil { + return nil, FactorNotFoundError{} + } + return factor, nil +} + // Change the factor simple name func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { f.UpdatedAt = time.Now() @@ -64,3 +80,15 @@ func (f *Factor) Enable(tx *storage.Connection) error { f.Enabled = true return tx.UpdateOnly(f, "enabled") } + +func findFactor(tx *storage.Connection, query string, args ...interface{}) (*Factor, error) { + obj := &Factor{} + if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, FactorNotFoundError{} + } + return nil, errors.Wrap(err, "error finding factor") + } + + return obj, nil +} From c1910736ced68fb6a454324138745d43c2dd130b Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 17 Jun 2022 12:11:37 +0800 Subject: [PATCH 028/180] fix: change method definitions --- api/mfa.go | 28 +++++++++++++++------------- models/challenge.go | 27 +++++++++++++++++---------- models/factor.go | 6 +++--- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index e0f69317b..c0e327504 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -39,11 +39,11 @@ type ChallengeFactorParams struct { } type ChallengeFactorResponse struct { - ChallengeID string - CreatedAt string - UpdatedAt string - ExpiredAt string - FactorID string + ID string + CreatedAt string + UpdatedAt string + ExpiresAt string + FactorID string } // RecoveryCodesResponse repreesnts a successful Backup code generation response @@ -153,6 +153,7 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { const FACTOR_PREFIX = "factor" const IMAGE_SIDE_LENGTH = 300 + var factor *models.Factor ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -225,10 +226,12 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { if !user.MFAEnabled { return MFANotEnabledError } + var factor *models.Factor + var err error params := &ChallengeFactorParams{} jsonDecoder := json.NewDecoder(r.Body) - err := jsonDecoder.Decode(params) + err = jsonDecoder.Decode(params) if err != nil { return badRequestError("Could not read EnrollFactor params: %v", err) } @@ -240,10 +243,9 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } if factorID != "" { - factor, err := models.FindByFactorID(factorID) - + factor, err = models.FindFactorByFactorID(a.db, factorID) } else if params.FactorSimpleName != "" { - factor, err := models.FindFactorBySimpleName(factorSimpleName) + factor, err = models.FindFactorBySimpleName(a.db, factorSimpleName) } if err != nil { if models.IsNotFoundError(err) { @@ -252,7 +254,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error finding factor").WithInternalError(err) } - challenge, terr := models.NewChallenge(factor) + challenge, terr := models.NewChallenge(factor.ID) if terr != nil { return internalServerError("Database error creating challenge").WithInternalError(err) } @@ -270,16 +272,16 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return nil }) - creationTime := challenge.CreatedAt + creationTime := challenge.CreatedAt.String() expiryTimeAsTimestamp, err := time.Parse(time.RFC3339, creationTime) if err != nil { return internalServerError("Error parsing database timestamp").WithInternalError(err) } - return sendJSON(w, http.StatusOK, *ChallengeFactorResponse{ + return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, CreatedAt: creationTime, - ExpiresAt: expiryTimeAsTimestamp.Add(time.Second * CHALLENGE_EXPIRY_DURATION), + ExpiresAt: expiryTimeAsTimestamp.Add(time.Second * CHALLENGE_EXPIRY_DURATION).String(), FactorID: factor.ID, }) } diff --git a/models/challenge.go b/models/challenge.go index a8f0927d2..813a4a09b 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -1,8 +1,12 @@ -package api +package models import ( + "database/sql" "fmt" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "time" ) type Challenge struct { @@ -11,20 +15,23 @@ type Challenge struct { CreatedAt *time.Time `json:"created_at" db:"created_at"` } -func (Challenge) TableName() string { - tableName := "mfa_challenges" -} - const CHALLENGE_PREFIX = "challenge" -func NewChallenge(factor *Factor) (*Challenge, error) { +func NewChallenge(factorID string) (*Challenge, error) { challenge := &Challenge{ ID: fmt.Sprintf("%s_%s", CHALLENGE_PREFIX, crypto.SecureToken()), - FactorID: factor.ID, + FactorID: factorID, } + return challenge, nil } -func FindChallengeByFactor(tx *storage.Connection, factor *Factor) ([]*Challenge, error) { - challenge := []*Challenge{} - +func FindChallengesByFactorID(tx *storage.Connection, factorID string) ([]*Challenge, error) { + challenges := []*Challenge{} + if err := tx.Q().Where("factor_id = ?", factorID, true).All(&challenges); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return challenges, nil + } + return nil, errors.Wrap(err, "Error finding MFA Challenges for factor") + } + return challenges, nil } diff --git a/models/factor.go b/models/factor.go index 04d6e28b4..1f079bda3 100644 --- a/models/factor.go +++ b/models/factor.go @@ -38,7 +38,7 @@ func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) ( } // FindFactorsByUser returns all factors belonging to a user -func FindFactorsByUser(tx *storage.Connection, user *User) (*Factor, error) { +func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { factors := []*Factor{} if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { if errors.Cause(err) == sql.ErrNoRows { @@ -49,7 +49,7 @@ func FindFactorsByUser(tx *storage.Connection, user *User) (*Factor, error) { return factors, nil } -func (f *Factor) FindFactorByFactorID(tx *storage.Connection, factorID string) (*Factor, error) { +func FindFactorByFactorID(tx *storage.Connection, factorID string) (*Factor, error) { factor, err := findFactor(tx, "id = ?", factorID) if err != nil { return nil, FactorNotFoundError{} @@ -57,7 +57,7 @@ func (f *Factor) FindFactorByFactorID(tx *storage.Connection, factorID string) ( return factor, nil } -func (f *Factor) FindFactorBySimpleName(tx *storage.Connection, simpleName string) ([]*Factor, error) { +func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, error) { factor, err := findFactor(tx, "factor_simple_name = ?", simpleName) if err != nil { return nil, FactorNotFoundError{} From 3027ef98f562f51aa8d098360d46c5ccc30a3bfc Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 20 Jun 2022 10:51:11 +0800 Subject: [PATCH 029/180] initial commit for verify factor --- api/api.go | 1 + api/mfa.go | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/api/api.go b/api/api.go index 242d82836..062cd8a01 100644 --- a/api/api.go +++ b/api/api.go @@ -191,6 +191,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) r.Post("/enroll_factor", api.EnrollFactor) r.Post("/challenge_factor", api.ChallengeFactor) + r.Post("/verify", api.VerifyFactor) }) }) }) diff --git a/api/mfa.go b/api/mfa.go index c0e327504..0be1abb97 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -38,6 +38,11 @@ type ChallengeFactorParams struct { FactorSimpleName string } +type VerifyParams struct { + ChallengeID string + Code string +} + type ChallengeFactorResponse struct { ID string CreatedAt string @@ -46,6 +51,14 @@ type ChallengeFactorResponse struct { FactorID string } +type VerfifyResponse struct { + Nonce string + ChallengeID stryying + MFAType string + Success string +} + + // RecoveryCodesResponse repreesnts a successful Backup code generation response type RecoveryCodesResponse struct { RecoveryCodes []string @@ -285,3 +298,30 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { FactorID: factor.ID, }) } + +func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { + // What about the webauthn case? + // TOTP -> call function to check code as + // var success + // Find the secret by factor and unhash it + // FindFactorByChallengeID(challenge_id) -> Get the secret + // + // valid := totp.Validate(passcode, key.Secret()) + // Transaction to fetch the secret + // Audit log that we are erading from DB + + // if valid { + // success = True + // // Continue on with life + // } else { + // return InvalidPasscodError + // } + + // Takes in: Challenge ID, Code + // Return + // challenge ID -> Should have a verified field + // Set a verified field + // mfaType + // success + +} From 55dee45ed74da2002debfefa5acd348c1cf14765 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 20 Jun 2022 17:06:06 +0800 Subject: [PATCH 030/180] test: add tests, remove factor_ prefix --- api/mfa.go | 10 ++-- .../20220607041349_add_mfa_schema.up.sql | 2 +- models/challenge.go | 17 ++++-- models/challenge_test.go | 59 +++++++++++++++++++ models/factor.go | 34 +++++------ models/factor_test.go | 34 ++++++++++- 6 files changed, 126 insertions(+), 30 deletions(-) create mode 100644 models/challenge_test.go diff --git a/api/mfa.go b/api/mfa.go index c0e327504..622f43a2e 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -15,9 +15,9 @@ import ( ) type EnrollFactorParams struct { - FactorSimpleName string `json:"factor_simple_name"` - FactorType string `json:"factor_type"` - Issuer string `json:"issuer"` + SimpleName string `json:"factor_simple_name"` + FactorType string `json:"factor_type"` + Issuer string `json:"issuer"` } type TOTPObject struct { @@ -191,7 +191,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) factorID := fmt.Sprintf("%s_%s", FACTOR_PREFIX, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, key.Secret()) + factor, terr := models.NewFactor(user, params.SimpleName, factorID, params.FactorType, key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -254,7 +254,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error finding factor").WithInternalError(err) } - challenge, terr := models.NewChallenge(factor.ID) + challenge, terr := models.NewChallenge(factor) if terr != nil { return internalServerError("Database error creating challenge").WithInternalError(err) } diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 8451a0b4e..20b77b286 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -9,7 +9,7 @@ END $$; CREATE TABLE IF NOT EXISTS auth.mfa_factors( id VARCHAR(256) NOT NULL, user_id uuid NOT NULL, - factor_simple_name VARCHAR(256) NULL, + simple_name VARCHAR(256) NULL, factor_type factor_type NOT NULL, enabled BOOLEAN NOT NULL, created_at timestamptz NOT NULL, diff --git a/models/challenge.go b/models/challenge.go index 813a4a09b..92c366e63 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -10,24 +10,29 @@ import ( ) type Challenge struct { - ID string `json:"challenge_id" db:"id"` - FactorID string `json:"factor_id" db:"factor_id"` - CreatedAt *time.Time `json:"created_at" db:"created_at"` + ID string `json:"challenge_id" db:"id"` + FactorID string `json:"factor_id" db:"factor_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` +} + +func (Challenge) TableName() string { + tableName := "mfa_challenges" + return tableName } const CHALLENGE_PREFIX = "challenge" -func NewChallenge(factorID string) (*Challenge, error) { +func NewChallenge(factor *Factor) (*Challenge, error) { challenge := &Challenge{ ID: fmt.Sprintf("%s_%s", CHALLENGE_PREFIX, crypto.SecureToken()), - FactorID: factorID, + FactorID: factor.ID, } return challenge, nil } func FindChallengesByFactorID(tx *storage.Connection, factorID string) ([]*Challenge, error) { challenges := []*Challenge{} - if err := tx.Q().Where("factor_id = ?", factorID, true).All(&challenges); err != nil { + if err := tx.Q().Where("factor_id = ?", factorID).All(&challenges); err != nil { if errors.Cause(err) == sql.ErrNoRows { return challenges, nil } diff --git a/models/challenge_test.go b/models/challenge_test.go new file mode 100644 index 000000000..23e4311c6 --- /dev/null +++ b/models/challenge_test.go @@ -0,0 +1,59 @@ +package models + +import ( + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/storage" + "github.com/netlify/gotrue/storage/test" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "testing" +) + +type ChallengeTestSuite struct { + suite.Suite + db *storage.Connection +} + +func TestChallenge(t *testing.T) { + globalConfig, err := conf.LoadGlobal(modelsTestConfig) + require.NoError(t, err) + + conn, err := test.SetupDBConnection(globalConfig) + require.NoError(t, err) + + ts := &ChallengeTestSuite{ + db: conn, + } + defer ts.db.Close() + + suite.Run(t, ts) +} + +func (ts *ChallengeTestSuite) SetupTest() { + TruncateAll(ts.db) +} + +func (ts *FactorTestSuite) TestFindChallengesByFactorID() { + u, err := NewUser(uuid.Nil, "", "genericemail@gmail.com", "secret", "test", nil) + require.NoError(ts.T(), err) + + err = ts.db.Create(u) + require.NoError(ts.T(), err) + + f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "phone", "topsecret") + require.NoError(ts.T(), err) + + err = ts.db.Create(f) + require.NoError(ts.T(), err) + + c, err := NewChallenge(f) + require.NoError(ts.T(), err) + + err = ts.db.Create(c) + require.NoError(ts.T(), err) + + n, err := FindChallengesByFactorID(ts.db, c.FactorID) + require.NoError(ts.T(), err) + require.Len(ts.T(), n, 1) +} diff --git a/models/factor.go b/models/factor.go index 1f079bda3..310541461 100644 --- a/models/factor.go +++ b/models/factor.go @@ -9,14 +9,14 @@ import ( ) type Factor struct { - UserID uuid.UUID `json` - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Enabled bool `json:"enabled" db:"enabled"` - FactorSimpleName string `json:"factor_simple_name" db:"factor_simple_name"` - SecretKey string `json:'-' db:'secret_key'` - FactorType string `json:"factor_type" db:"factor_type"` + UserID uuid.UUID `json:"user_id" db:"user_id"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Enabled bool `json:"enabled" db:"enabled"` + SimpleName string `json:"simple_name" db:"simple_name"` + SecretKey string `json:"secret_key" db:"secret_key"` + FactorType string `json:"factor_type" db:"factor_type"` } func (Factor) TableName() string { @@ -24,15 +24,15 @@ func (Factor) TableName() string { return tableName } -func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) (*Factor, error) { +func NewFactor(user *User, simpleName, id, factorType, secretKey string) (*Factor, error) { // TODO: Pass in secret and hash it using bcrypt or equiv factor := &Factor{ - ID: id, - UserID: user.ID, - Enabled: true, - FactorSimpleName: factorSimpleName, - SecretKey: secretKey, - FactorType: factorType, + ID: id, + UserID: user.ID, + Enabled: true, + SimpleName: simpleName, + SecretKey: secretKey, + FactorType: factorType, } return factor, nil } @@ -58,7 +58,7 @@ func FindFactorByFactorID(tx *storage.Connection, factorID string) (*Factor, err } func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, error) { - factor, err := findFactor(tx, "factor_simple_name = ?", simpleName) + factor, err := findFactor(tx, "simple_name = ?", simpleName) if err != nil { return nil, FactorNotFoundError{} } @@ -68,7 +68,7 @@ func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, // Change the factor simple name func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { f.UpdatedAt = time.Now() - return tx.UpdateOnly(f, "factor_simple_name", "updated_at") + return tx.UpdateOnly(f, "simple_name", "updated_at") } func (f *Factor) Disable(tx *storage.Connection) error { diff --git a/models/factor_test.go b/models/factor_test.go index c4ef2d495..7297ad186 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -38,7 +38,7 @@ func (ts *FactorTestSuite) TestToggleFactorEnabled() { u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "phone", "some-secret") require.NoError(ts.T(), err) require.NoError(ts.T(), f.Disable(ts.db)) @@ -51,3 +51,35 @@ func (ts *FactorTestSuite) TestToggleFactorEnabled() { require.Equal(ts.T(), true, f.Enabled) } + +func (ts *FactorTestSuite) TestFindFactorBySimpleName() { + f := ts.createFactor() + + n, err := FindFactorBySimpleName(ts.db, f.SimpleName) + require.NoError(ts.T(), err) + require.Equal(ts.T(), f.ID, n.ID) +} + +func (ts *FactorTestSuite) TestFindFactorByFactorID() { + f := ts.createFactor() + + n, err := FindFactorByFactorID(ts.db, f.ID) + require.NoError(ts.T(), err) + require.Equal(ts.T(), f.ID, n.ID) +} + +func (ts *FactorTestSuite) createFactor() *Factor { + user, err := NewUser(uuid.Nil, "", "agenericemail@gmail.com", "secret", "test", nil) + require.NoError(ts.T(), err) + + err = ts.db.Create(user) + require.NoError(ts.T(), err) + + factor, err := NewFactor(user, "asimplename", "factor-which-shall-not-be-named", "phone", "topsecret") + require.NoError(ts.T(), err) + + err = ts.db.Create(factor) + require.NoError(ts.T(), err) + + return factor +} From b1fa20a4d02c62804dabb7e2ef934ae14c807edc Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 21 Jun 2022 13:49:58 +0800 Subject: [PATCH 031/180] tests: add http test --- api/errors.go | 6 ++-- api/mfa.go | 40 +++++++++++++------------ api/mfa_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 22 deletions(-) diff --git a/api/errors.go b/api/errors.go index 2a701df5e..da7370c1e 100644 --- a/api/errors.go +++ b/api/errors.go @@ -18,9 +18,9 @@ var ( DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") // MFA Related errors - MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") - MFANotEnabled error = errors.New("MFA can only be disabled when it is Enabled") - MFANotEnabledError error = errors.New("MFA not enabled") + MFANotDisabled error = errors.New("MFA can only be enabled when it is Disabled") + MFANotEnabled error = errors.New("MFA can only be disabled when it is Enabled") + MFANotEnabledMsg = "MFA not enabled" ) var oauthErrorMap = map[int]string{ diff --git a/api/mfa.go b/api/mfa.go index 622f43a2e..12ba3a35b 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -15,7 +15,7 @@ import ( ) type EnrollFactorParams struct { - SimpleName string `json:"factor_simple_name"` + SimpleName string `json:"simple_name"` FactorType string `json:"factor_type"` Issuer string `json:"issuer"` } @@ -34,16 +34,17 @@ type EnrollFactorResponse struct { } type ChallengeFactorParams struct { - FactorID string - FactorSimpleName string + FactorID string `json:"factor_id"` + FactorSimpleName string `json:"factor_simple_name"` } type ChallengeFactorResponse struct { - ID string - CreatedAt string - UpdatedAt string - ExpiresAt string - FactorID string + ID string + CreatedAt string + UpdatedAt string + ExpiresAt string + FactorID string + FactorSimpleName string } // RecoveryCodesResponse repreesnts a successful Backup code generation response @@ -116,7 +117,7 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro user := getUser(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { - return MFANotEnabledError + forbiddenError(MFANotEnabledMsg) } now := time.Now() recoveryCodeModels := []*models.RecoveryCode{} @@ -153,12 +154,11 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { const FACTOR_PREFIX = "factor" const IMAGE_SIDE_LENGTH = 300 - var factor *models.Factor ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { - return MFANotEnabledError + return forbiddenError(MFANotEnabledMsg) } params := &EnrollFactorParams{} @@ -224,7 +224,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { user := getUser(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { - return MFANotEnabledError + return forbiddenError(MFANotEnabledMsg) } var factor *models.Factor var err error @@ -244,8 +244,10 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { if factorID != "" { factor, err = models.FindFactorByFactorID(a.db, factorID) - } else if params.FactorSimpleName != "" { + } else if factorSimpleName != "" { factor, err = models.FindFactorBySimpleName(a.db, factorSimpleName) + } else { + return unprocessableEntityError("Either FactorID or FactorSimpleName should be provided on signup.") } if err != nil { if models.IsNotFoundError(err) { @@ -272,16 +274,16 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return nil }) - creationTime := challenge.CreatedAt.String() - expiryTimeAsTimestamp, err := time.Parse(time.RFC3339, creationTime) + creationTime := challenge.CreatedAt if err != nil { return internalServerError("Error parsing database timestamp").WithInternalError(err) } return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ - ID: challenge.ID, - CreatedAt: creationTime, - ExpiresAt: expiryTimeAsTimestamp.Add(time.Second * CHALLENGE_EXPIRY_DURATION).String(), - FactorID: factor.ID, + ID: challenge.ID, + CreatedAt: creationTime.String(), + ExpiresAt: creationTime.Add(time.Second * CHALLENGE_EXPIRY_DURATION).String(), + FactorID: factor.ID, + FactorSimpleName: factor.SimpleName, }) } diff --git a/api/mfa_test.go b/api/mfa_test.go index b602002fe..62d41df37 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -42,6 +43,9 @@ func (ts *MFATestSuite) SetupTest() { u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "phone", "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") } func (ts *MFATestSuite) TestMFAEnable() { @@ -115,3 +119,78 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { numCodes := len(backupCodes) require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, numCodes) } + +func (ts *MFATestSuite) TestChallengeFactor() { + + cases := []struct { + desc string + id string + simpleName string + mfaEnabled bool + expectedCode int + }{ + { + "MFA Not Enabled", + "", + "", + false, + http.StatusForbidden, + }, + { + "Both Factor ID and Simple Name are present", + "testFactorID", + "testSimpleFactor", + true, + http.StatusUnprocessableEntity, + }, + { + "Only factor simple name", + "", + "testSimpleName", + true, + http.StatusOK, + }, + { + "Only factor ID", + "testFactorID", + "", + true, + http.StatusOK, + }, + { + "Both factor and simple name missing", + "", + "", + true, + http.StatusUnprocessableEntity, + }, + } + + for _, c := range cases { + ts.Run(c.desc, func() { + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) + + if c.mfaEnabled { + require.NoError(ts.T(), u.EnableMFA(ts.API.db), "Error setting MFA to disabled") + } + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err, "Error generating access token") + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "factor_id": c.id, + "factor_simple_name": c.simpleName, + })) + + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/mfa/%s/challenge_factor", u.ID), &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), c.expectedCode, w.Code) + }) + } +} From 4ea09865aaead146ee143471b71d8f6a3ad07bd9 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 21 Jun 2022 21:15:14 +0800 Subject: [PATCH 032/180] feat:initial verify endpoint --- api/mfa.go | 81 ++++++++++++++++++++++++++++++------------------ models/factor.go | 11 +++++++ 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 0be1abb97..35af031c3 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -38,9 +38,9 @@ type ChallengeFactorParams struct { FactorSimpleName string } -type VerifyParams struct { - ChallengeID string - Code string +type VerifyFactorParams struct { + ChallengeID string `json:"challenge_id"` + Code string `json:"code"` } type ChallengeFactorResponse struct { @@ -51,14 +51,12 @@ type ChallengeFactorResponse struct { FactorID string } -type VerfifyResponse struct { - Nonce string - ChallengeID stryying - MFAType string - Success string +type VerifyFactorResponse struct { + ChallengeID string + MFAType string + Success string } - // RecoveryCodesResponse repreesnts a successful Backup code generation response type RecoveryCodesResponse struct { RecoveryCodes []string @@ -300,28 +298,49 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { - // What about the webauthn case? - // TOTP -> call function to check code as - // var success - // Find the secret by factor and unhash it - // FindFactorByChallengeID(challenge_id) -> Get the secret + // TODO: Joel We need to attach a rate limiter to this so it doesn't get fuzzed + var err error + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if !user.MFAEnabled { + return MFANotEnabledError + } + params := &VerifyFactorParams{} + jsonDecoder := json.NewDecoder(r.Body) + err = jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Could not read VerifyFactor params: %v", err) + } + + factor, err := models.FindFactorByChallengeID(a.db, params.ChallengeID) + if err != nil { + if models.IsNotFoundError(err) { + return notFoundError(err.Error()) + } + return internalServerError("Database error finding factor").WithInternalError(err) + } + // No unhashing it + err = a.db.Transaction(func(tx *storage.Connection) error { + + if err = models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ + "factor_id": factor.ID, + "challenge_id": params.ChallengeID, + }); err != nil { + return err + } + return nil + }) // - // valid := totp.Validate(passcode, key.Secret()) - // Transaction to fetch the secret - // Audit log that we are erading from DB - - // if valid { - // success = True - // // Continue on with life - // } else { - // return InvalidPasscodError - // } - - // Takes in: Challenge ID, Code - // Return - // challenge ID -> Should have a verified field - // Set a verified field - // mfaType - // success + valid := totp.Validate(params.Code, factor.SecretKey) + if valid != true { + return unauthorizedError("Invalid code entered") + } + + return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ + ChallengeID: params.ChallengeID, + MFAType: factor.FactorType, + Success: fmt.Sprintf("%v", valid), + }) } diff --git a/models/factor.go b/models/factor.go index 1f079bda3..0383fac7f 100644 --- a/models/factor.go +++ b/models/factor.go @@ -65,6 +65,17 @@ func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, return factor, nil } +func FindFactorByChallengeID(tx *storage.Connection, factorID string) (*Factor, error) { + factor := &Factor{} + if err := tx.Q().Join("mfa_challenges", "mfa_factors.ID = mfa_challenges.factor_id").Where("id = ?", factorID).First(factor); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, FactorNotFoundError{} + } + return nil, errors.Wrap(err, "error finding factor") + } + return factor, nil +} + // Change the factor simple name func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { f.UpdatedAt = time.Now() From 023f5844338b79c5ac68d737853a505f5fd9cc75 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 22 Jun 2022 11:59:14 +0800 Subject: [PATCH 033/180] tests: add more tests --- .../20220607041349_add_mfa_schema.up.sql | 2 +- models/challenge.go | 11 ++++-- models/factor.go | 10 +++--- models/factor_test.go | 35 +++++++++++++++---- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 8451a0b4e..20b77b286 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -9,7 +9,7 @@ END $$; CREATE TABLE IF NOT EXISTS auth.mfa_factors( id VARCHAR(256) NOT NULL, user_id uuid NOT NULL, - factor_simple_name VARCHAR(256) NULL, + simple_name VARCHAR(256) NULL, factor_type factor_type NOT NULL, enabled BOOLEAN NOT NULL, created_at timestamptz NOT NULL, diff --git a/models/challenge.go b/models/challenge.go index 813a4a09b..588977a26 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -10,13 +10,18 @@ import ( ) type Challenge struct { - ID string `json:"challenge_id" db:"id"` - FactorID string `json:"factor_id" db:"factor_id"` - CreatedAt *time.Time `json:"created_at" db:"created_at"` + ID string `json:"challenge_id" db:"id"` + FactorID string `json:"factor_id" db:"factor_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` } const CHALLENGE_PREFIX = "challenge" +func (Challenge) TableName() string { + tableName := "mfa_challenges" + return tableName +} + func NewChallenge(factorID string) (*Challenge, error) { challenge := &Challenge{ ID: fmt.Sprintf("%s_%s", CHALLENGE_PREFIX, crypto.SecureToken()), diff --git a/models/factor.go b/models/factor.go index 0383fac7f..c32a2a218 100644 --- a/models/factor.go +++ b/models/factor.go @@ -9,13 +9,13 @@ import ( ) type Factor struct { - UserID uuid.UUID `json` + UserID uuid.UUID `json:"-" db:"user_id"` ID string `json:"id" db:"id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` Enabled bool `json:"enabled" db:"enabled"` - FactorSimpleName string `json:"factor_simple_name" db:"factor_simple_name"` - SecretKey string `json:'-' db:'secret_key'` + FactorSimpleName string `json:"simple_name" db:"simple_name"` + SecretKey string `json:"-" db:"secret_key"` FactorType string `json:"factor_type" db:"factor_type"` } @@ -65,9 +65,9 @@ func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, return factor, nil } -func FindFactorByChallengeID(tx *storage.Connection, factorID string) (*Factor, error) { +func FindFactorByChallengeID(tx *storage.Connection, challengeID string) (*Factor, error) { factor := &Factor{} - if err := tx.Q().Join("mfa_challenges", "mfa_factors.ID = mfa_challenges.factor_id").Where("id = ?", factorID).First(factor); err != nil { + if err := tx.Q().Join("mfa_challenges", "mfa_factors.ID = mfa_challenges.factor_id").Where("mfa_challenges.id= ?", challengeID).First(factor); err != nil { if errors.Cause(err) == sql.ErrNoRows { return nil, FactorNotFoundError{} } diff --git a/models/factor_test.go b/models/factor_test.go index c4ef2d495..2ac8ff72f 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -29,18 +29,25 @@ func TestFactor(t *testing.T) { suite.Run(t, ts) } +func (ts *FactorTestSuite) TestFindFactorByChallengeID() { + factor := ts.createFactor() + challenge, err := NewChallenge(factor.ID) + require.NoError(ts.T(), err) + + err = ts.db.Create(challenge) + require.NoError(ts.T(), err) + + n, err := FindFactorByChallengeID(ts.db, challenge.ID) + require.NoError(ts.T(), err) + require.Equal(ts.T(), factor.ID, n.ID) +} func (ts *FactorTestSuite) SetupTest() { TruncateAll(ts.db) } func (ts *FactorTestSuite) TestToggleFactorEnabled() { - u, err := NewUser(uuid.Nil, "", "", "", "", nil) - require.NoError(ts.T(), err) - - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "") - require.NoError(ts.T(), err) - + f := ts.createFactor() require.NoError(ts.T(), f.Disable(ts.db)) require.Equal(ts.T(), false, f.Enabled) @@ -51,3 +58,19 @@ func (ts *FactorTestSuite) TestToggleFactorEnabled() { require.Equal(ts.T(), true, f.Enabled) } + +func (ts *FactorTestSuite) createFactor() *Factor { + u, err := NewUser(uuid.Nil, "", "", "", "", nil) + require.NoError(ts.T(), err) + + err = ts.db.Create(u) + require.NoError(ts.T(), err) + + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "phone", "supersecretkey") + require.NoError(ts.T(), err) + + err = ts.db.Create(f) + require.NoError(ts.T(), err) + + return f +} From 285ac43ccb135fdca5278c4dfbb0667958fa0856 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 22 Jun 2022 12:36:18 +0800 Subject: [PATCH 034/180] refactor: remove whitespace changes and minor labels --- api/admin.go | 2 +- api/mfa.go | 5 +---- api/mfa_test.go | 19 +++++++------------ models/recovery_code_test.go | 12 ++++-------- 4 files changed, 13 insertions(+), 25 deletions(-) diff --git a/api/admin.go b/api/admin.go index 8c9c620f5..f9117fef5 100644 --- a/api/admin.go +++ b/api/admin.go @@ -175,7 +175,7 @@ func (a *API) adminUserUpdate(w http.ResponseWriter, r *http.Request) error { } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UserModifiedAction, "", map[string]interface{}{ "user_id": user.ID, "user_email": user.Email, "user_phone": user.Phone, diff --git a/api/mfa.go b/api/mfa.go index 059434f36..a02834409 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -10,7 +10,7 @@ import ( // RecoveryCodesResponse repreesnts a successful recovery code generation response type RecoveryCodesResponse struct { - RecoveryCodes []string + RecoveryCodes []string `json:"recovery_codes"` } func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { @@ -62,7 +62,6 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { const NUM_RECOVERY_CODES = 8 const RECOVERY_CODE_LENGTH = 8 - ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -75,7 +74,6 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro var recoveryCode string var recoveryCodes []string var recoveryCodeModel *models.RecoveryCode - for i := 0; i < NUM_RECOVERY_CODES; i++ { recoveryCode = crypto.SecureToken(RECOVERY_CODE_LENGTH) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode, &now) @@ -95,7 +93,6 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro } return nil }) - return sendJSON(w, http.StatusOK, &RecoveryCodesResponse{ RecoveryCodes: recoveryCodes, }) diff --git a/api/mfa_test.go b/api/mfa_test.go index 574679873..8149ca6d9 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -81,27 +81,22 @@ func (ts *MFATestSuite) TestMFADisable() { func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { const EXPECTED_NUM_OF_RECOVERY_CODES = 8 - u, err := models.NewUser(ts.instanceID, "", "test1@example.com", "test", ts.Config.JWT.Aud, nil) - u.EnableMFA() + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + ts.Require().NoError(err) + require.NoError(ts.T(), user.EnableMFA(ts.API.db)) - err = ts.API.db.Create(u) - require.NoError(ts.T(), err) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test1@example.com", ts.Config.JWT.Aud) - ts.Require().NoError(err) - w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/mfa/%s/generate_recovery_codes", user.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) data := make(map[string]interface{}) - require.Equal(ts.T(), http.StatusOK, w.Code) require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - recoveryCodes := data["RecoveryCodes"].([]interface{}) - numCodes := len(recoveryCodes) - require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, numCodes) + recoveryCodes := data["recovery_codes"].([]interface{}) + require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, len(recoveryCodes)) } diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 24c546878..092fbf00d 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -1,28 +1,24 @@ -package api +package models import ( "testing" - "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) - type RecoveryCodeTestSuite struct { - suite.Suite - db *storage.Connection + suite.Suite + db *storage.Connection } func (ts *RecoveryCodeTestSuite) SetupTest() { - TruncateAll(ts.db) + TruncateAll(ts.db) } - func TestRecoveryCode(t *testing.T) { globalConfig, err := conf.LoadGlobal(modelsTestConfig) require.NoError(t, err) From d26c6285e1a979c073fee760360944463b37f786 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 29 Jun 2022 21:55:47 +0800 Subject: [PATCH 035/180] fix: update db schema --- migrations/20220607041349_add_mfa_schema.up.sql | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 23ac349f7..9a708c911 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -1,6 +1,6 @@ -- See: https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql/48382296#48382296 DO $$ BEGIN - CREATE TYPE factor_type AS ENUM('phone', 'webauthn'); + CREATE TYPE factor_type AS ENUM('totp', 'webauthn'); EXCEPTION WHEN duplicate_object THEN null; END $$; @@ -9,14 +9,15 @@ END $$; CREATE TABLE IF NOT EXISTS auth.mfa_factors( id VARCHAR(256) NOT NULL, user_id uuid NOT NULL, - factor_simple_name VARCHAR(256) NULL, + simple_name VARCHAR(256) NULL, factor_type factor_type NOT NULL, enabled BOOLEAN NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, secret_key VARCHAR(256) NOT NULL, + UNIQUE(user_id, simple_name), CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), - CONSTRAINT mfa_factors FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE + CONSTRAINT mfa_factors_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); comment on table auth.mfa_factors is 'Auth: stores metadata about factors'; @@ -38,8 +39,8 @@ CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( valid BOOLEAN NOT NULL, created_at timestamptz NOT NULL, used_at timestamptz NOT NULL, - CONSTRAINT mfa_recovery_codes_user_id_recovery_code_key UNIQUE(user_id, recovery_code), - CONSTRAINT mfa_recovery_codes FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE + CONSTRAINT mfa_recovery_codes_user_id_recovery_code_pkey UNIQUE(user_id, recovery_code), + CONSTRAINT mfa_recovery_codes_user_id_fkey FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); comment on table auth.mfa_recovery_codes is 'Auth: stores recovery codes for Multi Factor Authentication'; From e98ff1ea9e30091d0ef2d5a826adf5d6c0501a8e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 09:39:25 +0800 Subject: [PATCH 036/180] refactor: add states for factor_status --- migrations/20220607041349_add_mfa_schema.up.sql | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 9a708c911..178bb7a99 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -1,6 +1,7 @@ -- See: https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql/48382296#48382296 DO $$ BEGIN CREATE TYPE factor_type AS ENUM('totp', 'webauthn'); + CREATE TYPE factor_status AS ENUM('enabled', 'disabled', 'unverified', 'verified') EXCEPTION WHEN duplicate_object THEN null; END $$; @@ -9,13 +10,13 @@ END $$; CREATE TABLE IF NOT EXISTS auth.mfa_factors( id VARCHAR(256) NOT NULL, user_id uuid NOT NULL, - simple_name VARCHAR(256) NULL, + friendly_name VARCHAR(256) NULL, factor_type factor_type NOT NULL, - enabled BOOLEAN NOT NULL, + status factor_status NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, secret_key VARCHAR(256) NOT NULL, - UNIQUE(user_id, simple_name), + UNIQUE(user_id, friendly_name), CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), CONSTRAINT mfa_factors_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); From 77c1fa869d81fb09b58e078772f21c631949eab1 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 09:56:07 +0800 Subject: [PATCH 037/180] fix: update statuses --- migrations/20220607041349_add_mfa_schema.up.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 178bb7a99..64988c464 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -1,7 +1,7 @@ -- See: https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql/48382296#48382296 DO $$ BEGIN CREATE TYPE factor_type AS ENUM('totp', 'webauthn'); - CREATE TYPE factor_status AS ENUM('enabled', 'disabled', 'unverified', 'verified') + CREATE TYPE factor_status AS ENUM('disabled', 'unverified', 'verified') EXCEPTION WHEN duplicate_object THEN null; END $$; @@ -27,6 +27,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( id VARCHAR(256) NOT NULL, factor_id VARCHAR(256) NOT NULL, created_at timestamptz NOT NULL, + verified BOOLEAN NOT NULL, CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id), CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE ); From 1bebb3982292b6cd0f6bdc9be239b39435b228ee Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 09:58:17 +0800 Subject: [PATCH 038/180] fix: add semicolon --- migrations/20220607041349_add_mfa_schema.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 64988c464..0672a9d1f 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -1,7 +1,7 @@ -- See: https://stackoverflow.com/questions/7624919/check-if-a-user-defined-type-already-exists-in-postgresql/48382296#48382296 DO $$ BEGIN CREATE TYPE factor_type AS ENUM('totp', 'webauthn'); - CREATE TYPE factor_status AS ENUM('disabled', 'unverified', 'verified') + CREATE TYPE factor_status AS ENUM('disabled', 'unverified', 'verified'); EXCEPTION WHEN duplicate_object THEN null; END $$; From 684e4fc5b0272783422d58b26c944586bd4de065 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 16:26:54 +0800 Subject: [PATCH 039/180] refactor: change bools to timestamps --- migrations/20220607041349_add_mfa_schema.up.sql | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index 0672a9d1f..f6f0c7b0b 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -8,14 +8,14 @@ END $$; -- auth.mfa_factors definition CREATE TABLE IF NOT EXISTS auth.mfa_factors( - id VARCHAR(256) NOT NULL, + id VARCHAR(255) NOT NULL, user_id uuid NOT NULL, - friendly_name VARCHAR(256) NULL, + friendly_name VARCHAR(255) NULL, factor_type factor_type NOT NULL, status factor_status NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, - secret_key VARCHAR(256) NOT NULL, + secret_key VARCHAR(255) NOT NULL, UNIQUE(user_id, friendly_name), CONSTRAINT mfa_factors_pkey PRIMARY KEY(id), CONSTRAINT mfa_factors_user_id_fkey FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE @@ -24,10 +24,10 @@ comment on table auth.mfa_factors is 'Auth: stores metadata about factors'; -- auth.mfa_challenges definition CREATE TABLE IF NOT EXISTS auth.mfa_challenges( - id VARCHAR(256) NOT NULL, - factor_id VARCHAR(256) NOT NULL, + id VARCHAR(255) NOT NULL, + factor_id VARCHAR(255) NOT NULL, created_at timestamptz NOT NULL, - verified BOOLEAN NOT NULL, + verified_at timestamptz NOT NULL, CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id), CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE ); @@ -35,10 +35,9 @@ comment on table auth.mfa_challenges is 'Auth: stores metadata about challenge r -- auth.mfa_recovery_codes definition CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( - id serial PRIMARY KEY, + id bigint generated always as identity, user_id uuid NOT NULL, recovery_code VARCHAR(32) NOT NULL, - valid BOOLEAN NOT NULL, created_at timestamptz NOT NULL, used_at timestamptz NOT NULL, CONSTRAINT mfa_recovery_codes_user_id_recovery_code_pkey UNIQUE(user_id, recovery_code), From 76e42aae4d8589a96bad9e1a33328f154afd1d50 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 16:34:17 +0800 Subject: [PATCH 040/180] refactor: drop _mfa suffix --- api/api.go | 4 ++-- api/mfa_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/api.go b/api/api.go index 884b64a32..8c6a86115 100644 --- a/api/api.go +++ b/api/api.go @@ -186,8 +186,8 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/mfa", func(r *router) { r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) - r.Put("/disable_mfa", api.DisableMFA) - r.Put("/enable_mfa", api.EnableMFA) + r.Put("/disable", api.DisableMFA) + r.Put("/enable", api.EnableMFA) }) }) }) diff --git a/api/mfa_test.go b/api/mfa_test.go index bc84ac7ba..42eb28e8f 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -49,7 +49,7 @@ func (ts *MFATestSuite) TestMFAEnable() { token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/enable_mfa", u.ID), nil) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/enable", u.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) @@ -66,7 +66,7 @@ func (ts *MFATestSuite) TestMFADisable() { token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/disable_mfa", u.ID), nil) + req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/disable", u.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) From aa81ee815f5bd402920489847f562a667050fa69 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 21:28:23 +0800 Subject: [PATCH 041/180] refactor: change recovery code ID type to uuid --- api/mfa.go | 7 +++-- .../20220607041349_add_mfa_schema.up.sql | 6 ++-- models/recovery_code.go | 28 +++++++------------ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index a02834409..a9d35cf24 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -84,8 +84,10 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro recoveryCodeModels = append(recoveryCodeModels, recoveryCodeModel) } terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr = tx.Create(recoveryCodeModels); terr != nil { - return terr + for _, recoveryCodeModel := range recoveryCodeModels { + if terr = tx.Create(recoveryCodeModel); terr != nil { + return terr + } } if terr := models.NewAuditLogEntry(tx, instanceID, user, models.GenerateRecoveryCodesAction, r.RemoteAddr, nil); terr != nil { @@ -93,6 +95,7 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro } return nil }) + return sendJSON(w, http.StatusOK, &RecoveryCodesResponse{ RecoveryCodes: recoveryCodes, }) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index f6f0c7b0b..b0e286dea 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -27,7 +27,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_challenges( id VARCHAR(255) NOT NULL, factor_id VARCHAR(255) NOT NULL, created_at timestamptz NOT NULL, - verified_at timestamptz NOT NULL, + verified_at timestamptz NULL, CONSTRAINT mfa_challenges_pkey PRIMARY KEY (id), CONSTRAINT mfa_challenges_auth_factor_id_fkey FOREIGN KEY (factor_id) REFERENCES auth.mfa_factors(id) ON DELETE CASCADE ); @@ -35,11 +35,11 @@ comment on table auth.mfa_challenges is 'Auth: stores metadata about challenge r -- auth.mfa_recovery_codes definition CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( - id bigint generated always as identity, + id uuid NOT NULL, user_id uuid NOT NULL, recovery_code VARCHAR(32) NOT NULL, created_at timestamptz NOT NULL, - used_at timestamptz NOT NULL, + verified_at timestamptz NULL, CONSTRAINT mfa_recovery_codes_user_id_recovery_code_pkey UNIQUE(user_id, recovery_code), CONSTRAINT mfa_recovery_codes_user_id_fkey FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); diff --git a/models/recovery_code.go b/models/recovery_code.go index e3927f372..847c5c099 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -3,37 +3,38 @@ package models import ( "database/sql" "github.com/gofrs/uuid" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" "github.com/pkg/errors" - "golang.org/x/crypto/bcrypt" "time" ) type RecoveryCode struct { + ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` CreatedAt *time.Time `json:"created_at" db:"created_at"` RecoveryCode string `json:"recovery_code" db:"recovery_code"` - Valid bool `json:"valid" db:"valid"` - TimeUsed time.Time `json:"time_used" db:"time_used"` + VerifiedAt *time.Time `json:"verified_at" db:"verified_at"` } func (RecoveryCode) TableName() string { - tableName := "recovery_codes" + tableName := "mfa_recovery_codes" return tableName } // Returns a new recovery code associated with the user func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*RecoveryCode, error) { - rc, err := hashRecoveryCode(recoveryCode) + tokenLength := 10 + + id, err := uuid.NewV4() if err != nil { - return nil, err + return nil, errors.Wrap(err, "Error generating unique id") } - code := &RecoveryCode{ + ID: id, UserID: user.ID, - RecoveryCode: rc, + RecoveryCode: crypto.SecureToken(tokenLength), CreatedAt: now, - Valid: true, } return code, nil @@ -50,12 +51,3 @@ func FindValidRecoveryCodesByUser(tx *storage.Connection, user *User) ([]*Recove } return recoveryCodes, nil } - -// hashRecoveryCode generates a hashed recoveryCode from a plaintext string -func hashRecoveryCode(recoveryCode string) (string, error) { - rc, err := bcrypt.GenerateFromPassword([]byte(recoveryCode), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(rc), nil -} From 8948b717e3df33b163b26e66872c4d19c792efd7 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 21:33:03 +0800 Subject: [PATCH 042/180] fix: set length of token back to 8 --- api/api.go | 2 +- models/recovery_code.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/api/api.go b/api/api.go index bbcdd30eb..71e0d472b 100644 --- a/api/api.go +++ b/api/api.go @@ -188,7 +188,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadUser) r.Put("/disable", api.DisableMFA) r.Put("/enable", api.EnableMFA) - r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) + r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) }) }) }) diff --git a/models/recovery_code.go b/models/recovery_code.go index 847c5c099..7ebc01665 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -3,7 +3,6 @@ package models import ( "database/sql" "github.com/gofrs/uuid" - "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" "github.com/pkg/errors" "time" @@ -24,7 +23,6 @@ func (RecoveryCode) TableName() string { // Returns a new recovery code associated with the user func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*RecoveryCode, error) { - tokenLength := 10 id, err := uuid.NewV4() if err != nil { @@ -33,7 +31,7 @@ func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*Recovery code := &RecoveryCode{ ID: id, UserID: user.ID, - RecoveryCode: crypto.SecureToken(tokenLength), + RecoveryCode: recoveryCode, CreatedAt: now, } From b025bddd65d705057dcd8e26d6e311a5b9b945b1 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 30 Jun 2022 21:39:14 +0800 Subject: [PATCH 043/180] refactor: revert code consumption time to used_at --- migrations/20220607041349_add_mfa_schema.up.sql | 2 +- models/recovery_code.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index b0e286dea..c3c3c7be6 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( user_id uuid NOT NULL, recovery_code VARCHAR(32) NOT NULL, created_at timestamptz NOT NULL, - verified_at timestamptz NULL, + used_at timestamptz NULL, CONSTRAINT mfa_recovery_codes_user_id_recovery_code_pkey UNIQUE(user_id, recovery_code), CONSTRAINT mfa_recovery_codes_user_id_fkey FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); diff --git a/models/recovery_code.go b/models/recovery_code.go index 7ebc01665..de2ce6078 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -13,7 +13,7 @@ type RecoveryCode struct { UserID uuid.UUID `json:"user_id" db:"user_id"` CreatedAt *time.Time `json:"created_at" db:"created_at"` RecoveryCode string `json:"recovery_code" db:"recovery_code"` - VerifiedAt *time.Time `json:"verified_at" db:"verified_at"` + UsedAt *time.Time `json:"used_at" db:"used_at"` } func (RecoveryCode) TableName() string { From 23090cdd26635c2182cfdb5c31083ac66931ccf4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Jul 2022 17:14:30 +0800 Subject: [PATCH 044/180] test: add test for FindValidRecoveryCodesByUser --- api/mfa.go | 4 +--- models/recovery_code.go | 8 +++----- models/recovery_code_test.go | 37 +++++++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index a9d35cf24..24de33c1c 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -5,7 +5,6 @@ import ( "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" "net/http" - "time" ) // RecoveryCodesResponse repreesnts a successful recovery code generation response @@ -68,7 +67,6 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro if !user.MFAEnabled { return MFANotEnabledError } - now := time.Now() recoveryCodeModels := []*models.RecoveryCode{} var terr error var recoveryCode string @@ -76,7 +74,7 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro var recoveryCodeModel *models.RecoveryCode for i := 0; i < NUM_RECOVERY_CODES; i++ { recoveryCode = crypto.SecureToken(RECOVERY_CODE_LENGTH) - recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode, &now) + recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) if terr != nil { return internalServerError("Error creating recovery code").WithInternalError(terr) } diff --git a/models/recovery_code.go b/models/recovery_code.go index de2ce6078..e63e6a2c3 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -11,7 +11,7 @@ import ( type RecoveryCode struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` - CreatedAt *time.Time `json:"created_at" db:"created_at"` + CreatedAt time.Time `json:"created_at" db:"created_at"` RecoveryCode string `json:"recovery_code" db:"recovery_code"` UsedAt *time.Time `json:"used_at" db:"used_at"` } @@ -22,8 +22,7 @@ func (RecoveryCode) TableName() string { } // Returns a new recovery code associated with the user -func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*RecoveryCode, error) { - +func NewRecoveryCode(user *User, recoveryCode string) (*RecoveryCode, error) { id, err := uuid.NewV4() if err != nil { return nil, errors.Wrap(err, "Error generating unique id") @@ -32,7 +31,6 @@ func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*Recovery ID: id, UserID: user.ID, RecoveryCode: recoveryCode, - CreatedAt: now, } return code, nil @@ -41,7 +39,7 @@ func NewRecoveryCode(user *User, recoveryCode string, now *time.Time) (*Recovery // FindValidRecoveryCodes returns all valid recovery codes associated to a user func FindValidRecoveryCodesByUser(tx *storage.Connection, user *User) ([]*RecoveryCode, error) { recoveryCodes := []*RecoveryCode{} - if err := tx.Q().Where("user_id = ? AND valid = ?", user.ID, true).All(&recoveryCodes); err != nil { + if err := tx.Q().Where("user_id = ? AND used_at IS NOT NULL", user.ID).All(&recoveryCodes); err != nil { if errors.Cause(err) == sql.ErrNoRows { return recoveryCodes, nil } diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 092fbf00d..8ea8ecfbd 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -2,7 +2,9 @@ package models import ( "testing" - + "fmt" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" @@ -17,6 +19,9 @@ type RecoveryCodeTestSuite struct { func (ts *RecoveryCodeTestSuite) SetupTest() { TruncateAll(ts.db) + + + } func TestRecoveryCode(t *testing.T) { @@ -33,3 +38,33 @@ func TestRecoveryCode(t *testing.T) { suite.Run(t, ts) } + + +func (ts *RecoveryCodeTestSuite)TestFindValidRecoveryCodesByUser() { + numRecoveryCodes := 8 + var expectedRecoveryCodes []string + user, err := NewUser(uuid.Nil, "", "", "", "", nil) + err = ts.db.Create(user) + require.NoError(ts.T(), err) + for i:=0;i <= numRecoveryCodes;i++ { + rc := ts.createRecoveryCode(user) + expectedRecoveryCodes = append(expectedRecoveryCodes, rc.RecoveryCode) + } + recoveryCodes, err:= FindValidRecoveryCodesByUser(ts.db, user) + require.NoError(ts.T(), err) + require.Equal(ts.T(), numRecoveryCodes, len(recoveryCodes), fmt.Sprintf("Expected %d recovery codes but got %d", numRecoveryCodes, len(recoveryCodes))) + +for index, recoveryCode := range(recoveryCodes) { + require.Equal(ts.T(), expectedRecoveryCodes[index], recoveryCode, "Recovery codes should match") + } +} + + +func (ts *RecoveryCodeTestSuite) createRecoveryCode(u *User) *RecoveryCode { + recoveryCodeLength := 8 + rc, err := NewRecoveryCode(u, crypto.SecureToken(recoveryCodeLength)) + require.NoError(ts.T(), err) + err = ts.db.Create(rc) + require.NoError(ts.T(), err) + return rc + } From f3ac1066aca52baedaf0ef54a69caa22f0422fec Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Jul 2022 17:31:30 +0800 Subject: [PATCH 045/180] refactor: convert var names to lowercase --- api/mfa.go | 8 ++++---- models/recovery_code_test.go | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 24de33c1c..a8b9208b9 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -59,8 +59,8 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { } func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { - const NUM_RECOVERY_CODES = 8 - const RECOVERY_CODE_LENGTH = 8 + const numRecoveryCodes = 8 + const recoveryCodeLength = 8 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -72,8 +72,8 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro var recoveryCode string var recoveryCodes []string var recoveryCodeModel *models.RecoveryCode - for i := 0; i < NUM_RECOVERY_CODES; i++ { - recoveryCode = crypto.SecureToken(RECOVERY_CODE_LENGTH) + for i := 0; i < numRecoveryCodes; i++ { + recoveryCode = crypto.SecureToken(recoveryCodeLength) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) if terr != nil { return internalServerError("Error creating recovery code").WithInternalError(terr) diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 8ea8ecfbd..83a780c21 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -41,6 +41,7 @@ func TestRecoveryCode(t *testing.T) { func (ts *RecoveryCodeTestSuite)TestFindValidRecoveryCodesByUser() { + // TODO: Joel -- convert numRecoveryCodes and recoveryCodeLength into constants in mfa.go numRecoveryCodes := 8 var expectedRecoveryCodes []string user, err := NewUser(uuid.Nil, "", "", "", "", nil) From 5e279833a41876ed2487026efea2a00ab13380c9 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Jul 2022 17:36:13 +0800 Subject: [PATCH 046/180] fix: add error check at end of GenerateRecoveryCodes --- api/mfa.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/mfa.go b/api/mfa.go index a8b9208b9..62fcbf175 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -93,6 +93,9 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro } return nil }) + if terr != nil { + return terr + } return sendJSON(w, http.StatusOK, &RecoveryCodesResponse{ RecoveryCodes: recoveryCodes, From 9d0618f332a6d23514c1d35d7c6e489d55165141 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Jul 2022 17:39:26 +0800 Subject: [PATCH 047/180] chore: run gofmt -s -w --- api/admin.go | 2 +- api/audit.go | 6 +++--- api/hook_test.go | 2 +- models/instance.go | 4 ++-- models/recovery_code.go | 4 ++-- models/recovery_code_test.go | 32 ++++++++++++++------------------ models/user_test.go | 2 +- 7 files changed, 24 insertions(+), 28 deletions(-) diff --git a/api/admin.go b/api/admin.go index f9117fef5..2134ce2d3 100644 --- a/api/admin.go +++ b/api/admin.go @@ -68,7 +68,7 @@ func (a *API) adminUsers(w http.ResponseWriter, r *http.Request) error { return badRequestError("Bad Pagination Parameters: %v", err) } - sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{models.SortField{Name: models.CreatedAt, Dir: models.Descending}}) + sortParams, err := sort(r, map[string]bool{models.CreatedAt: true}, []models.SortField{{Name: models.CreatedAt, Dir: models.Descending}}) if err != nil { return badRequestError("Bad Sort Parameters: %v", err) } diff --git a/api/audit.go b/api/audit.go index 13218bbe0..b7b954512 100644 --- a/api/audit.go +++ b/api/audit.go @@ -8,9 +8,9 @@ import ( ) var filterColumnMap = map[string][]string{ - "author": []string{"actor_username", "actor_name"}, - "action": []string{"action"}, - "type": []string{"log_type"}, + "author": {"actor_username", "actor_name"}, + "action": {"action"}, + "type": {"log_type"}, } func (a *API) adminAuditLog(w http.ResponseWriter, r *http.Request) error { diff --git a/api/hook_test.go b/api/hook_test.go index 26a6e7ce1..fb6a35932 100644 --- a/api/hook_test.go +++ b/api/hook_test.go @@ -99,7 +99,7 @@ func TestSignupHookFromClaims(t *testing.T) { ctx := context.Background() ctx = withFunctionHooks(ctx, map[string][]string{ - "signup": []string{svr.URL}, + "signup": {svr.URL}, }) require.NoError(t, triggerEventHooks(ctx, conn, SignupEvent, user, iid, config)) diff --git a/models/instance.go b/models/instance.go index 009c456a6..4cd2ba7f0 100644 --- a/models/instance.go +++ b/models/instance.go @@ -74,8 +74,8 @@ func GetInstanceByUUID(tx *storage.Connection, uuid uuid.UUID) (*Instance, error func DeleteInstance(conn *storage.Connection, instance *Instance) error { return conn.Transaction(func(tx *storage.Connection) error { delModels := map[string]*pop.Model{ - "user": &pop.Model{Value: &User{}}, - "refresh token": &pop.Model{Value: &RefreshToken{}}, + "user": {Value: &User{}}, + "refresh token": {Value: &RefreshToken{}}, } for name, dm := range delModels { diff --git a/models/recovery_code.go b/models/recovery_code.go index e63e6a2c3..244935982 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -11,9 +11,9 @@ import ( type RecoveryCode struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + CreatedAt time.Time `json:"created_at" db:"created_at"` RecoveryCode string `json:"recovery_code" db:"recovery_code"` - UsedAt *time.Time `json:"used_at" db:"used_at"` + UsedAt *time.Time `json:"used_at" db:"used_at"` } func (RecoveryCode) TableName() string { diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 83a780c21..6dd5848d8 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -1,15 +1,15 @@ package models import ( - "testing" "fmt" "github.com/gofrs/uuid" - "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "testing" ) type RecoveryCodeTestSuite struct { @@ -20,8 +20,6 @@ type RecoveryCodeTestSuite struct { func (ts *RecoveryCodeTestSuite) SetupTest() { TruncateAll(ts.db) - - } func TestRecoveryCode(t *testing.T) { @@ -39,33 +37,31 @@ func TestRecoveryCode(t *testing.T) { suite.Run(t, ts) } - -func (ts *RecoveryCodeTestSuite)TestFindValidRecoveryCodesByUser() { +func (ts *RecoveryCodeTestSuite) TestFindValidRecoveryCodesByUser() { // TODO: Joel -- convert numRecoveryCodes and recoveryCodeLength into constants in mfa.go numRecoveryCodes := 8 var expectedRecoveryCodes []string user, err := NewUser(uuid.Nil, "", "", "", "", nil) err = ts.db.Create(user) - require.NoError(ts.T(), err) - for i:=0;i <= numRecoveryCodes;i++ { + require.NoError(ts.T(), err) + for i := 0; i <= numRecoveryCodes; i++ { rc := ts.createRecoveryCode(user) expectedRecoveryCodes = append(expectedRecoveryCodes, rc.RecoveryCode) } - recoveryCodes, err:= FindValidRecoveryCodesByUser(ts.db, user) - require.NoError(ts.T(), err) + recoveryCodes, err := FindValidRecoveryCodesByUser(ts.db, user) + require.NoError(ts.T(), err) require.Equal(ts.T(), numRecoveryCodes, len(recoveryCodes), fmt.Sprintf("Expected %d recovery codes but got %d", numRecoveryCodes, len(recoveryCodes))) -for index, recoveryCode := range(recoveryCodes) { + for index, recoveryCode := range recoveryCodes { require.Equal(ts.T(), expectedRecoveryCodes[index], recoveryCode, "Recovery codes should match") } } - func (ts *RecoveryCodeTestSuite) createRecoveryCode(u *User) *RecoveryCode { recoveryCodeLength := 8 - rc, err := NewRecoveryCode(u, crypto.SecureToken(recoveryCodeLength)) - require.NoError(ts.T(), err) - err = ts.db.Create(rc) - require.NoError(ts.T(), err) - return rc - } + rc, err := NewRecoveryCode(u, crypto.SecureToken(recoveryCodeLength)) + require.NoError(ts.T(), err) + err = ts.db.Create(rc) + require.NoError(ts.T(), err) + return rc +} diff --git a/models/user_test.go b/models/user_test.go index a24066e14..06d08d733 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -125,7 +125,7 @@ func (ts *UserTestSuite) TestFindUsersInAudience() { sp := &SortParams{ Fields: []SortField{ - SortField{Name: "created_at", Dir: Descending}, + {Name: "created_at", Dir: Descending}, }, } n, err = FindUsersInAudience(ts.db, u.InstanceID, u.Aud, nil, sp, "") From de3184a0f7404f3e80b2b3440f573515a1527f68 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 1 Jul 2022 22:20:46 +0800 Subject: [PATCH 048/180] refactor: update tests with new naming scheme --- api/mfa.go | 3 ++- models/factor.go | 51 +++++++++++++++++++------------------------ models/factor_test.go | 24 +++++++++++++------- 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index f6159b950..5636a4df8 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -168,7 +168,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) factorID := fmt.Sprintf("%s_%s", FACTOR_PREFIX, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, key.Secret()) + // TODO(Joel): Convert this into an Enum + factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, "disabled", key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } diff --git a/models/factor.go b/models/factor.go index 41006dee2..52e10cbc0 100644 --- a/models/factor.go +++ b/models/factor.go @@ -9,14 +9,14 @@ import ( ) type Factor struct { - UserID uuid.UUID `json` - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Enabled bool `json:"enabled" db:"enabled"` - FactorSimpleName string `json:"factor_simple_name" db:"factor_simple_name"` - SecretKey string `json:'-' db:'secret_key'` - FactorType string `json:"factor_type" db:"factor_type"` + UserID uuid.UUID `json: "user_id" db:"user_id"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Status string `json:"status" db:"status"` + FriendlyName string `json:"friendly_name" db:"friendly_name"` + SecretKey string `json:'-' db:'secret_key'` + FactorType string `json:"factor_type" db:"factor_type"` } func (Factor) TableName() string { @@ -24,15 +24,14 @@ func (Factor) TableName() string { return tableName } -func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) (*Factor, error) { - // TODO: Pass in secret and hash it using bcrypt or equiv +func NewFactor(user *User, friendlyName, id, factorType, status, secretKey string) (*Factor, error) { factor := &Factor{ - ID: id, - UserID: user.ID, - Enabled: true, - FactorSimpleName: factorSimpleName, - SecretKey: secretKey, - FactorType: factorType, + ID: id, + UserID: user.ID, + Status: status, + FriendlyName: friendlyName, + SecretKey: secretKey, + FactorType: factorType, } return factor, nil } @@ -49,18 +48,14 @@ func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { return factors, nil } -// Change the factor simple name -func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { - f.UpdatedAt = time.Now() - return tx.UpdateOnly(f, "factor_simple_name", "updated_at") +// Change the friendly name +func (f *Factor) UpdateFriendlyName(tx *storage.Connection, friendlyName string) error { + f.FriendlyName = friendlyName + return tx.UpdateOnly(f, "friendly_name", "updated_at") } -func (f *Factor) Disable(tx *storage.Connection) error { - f.Enabled = false - return tx.UpdateOnly(f, "enabled") -} - -func (f *Factor) Enable(tx *storage.Connection) error { - f.Enabled = true - return tx.UpdateOnly(f, "enabled") +//Change the factor status +func (f *Factor) UpdateStatus(tx *storage.Connection, status string) error { + f.Status = status + return tx.UpdateOnly(f, "status", "updated_at") } diff --git a/models/factor_test.go b/models/factor_test.go index c4ef2d495..a7a73d9e7 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -34,20 +34,28 @@ func (ts *FactorTestSuite) SetupTest() { TruncateAll(ts.db) } -func (ts *FactorTestSuite) TestToggleFactorEnabled() { +func (ts *FactorTestSuite) TestUpdateStatus() { + newFactorStatus := "verified" u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "disabled", "") require.NoError(ts.T(), err) - require.NoError(ts.T(), f.Disable(ts.db)) - require.Equal(ts.T(), false, f.Enabled) + require.NoError(ts.T(), f.UpdateStatus(ts.db, newFactorStatus)) + require.Equal(ts.T(), newFactorStatus, f.Status) +} + +func (ts *FactorTestSuite) TestUpdateFriendlyName() { + newSimpleName := "newFactorName" + + u, err := NewUser(uuid.Nil, "", "", "", "", nil) + require.NoError(ts.T(), err) - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "disabled", "") + require.NoError(ts.T(), err) - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) + require.NoError(ts.T(), f.UpdateFriendlyName(ts.db, newSimpleName)) + require.Equal(ts.T(), newSimpleName, f.FriendlyName) } From c653135ef3b27cd144afd81f377fb3181913c03d Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sat, 2 Jul 2022 14:42:05 +0800 Subject: [PATCH 049/180] fix: update tests with new names --- api/errors.go | 2 +- api/mfa.go | 49 +++++++++++++++++++------------------- api/mfa_test.go | 8 +++---- models/challenge_test.go | 2 +- models/factor.go | 51 ++++++++++++++++++---------------------- models/factor_test.go | 24 +++---------------- 6 files changed, 57 insertions(+), 79 deletions(-) diff --git a/api/errors.go b/api/errors.go index 64ac708f8..72653f964 100644 --- a/api/errors.go +++ b/api/errors.go @@ -18,7 +18,7 @@ var ( DuplicatePhoneMsg = "A user with this phone number has already been registered" UserExistsError error = errors.New("User already exists") // MFA Related errors - MFANotEnabledMsg = "MFA not enabled" + MFANotEnabledMsg = "MFA not enabled" ) var oauthErrorMap = map[int]string{ diff --git a/api/mfa.go b/api/mfa.go index 2ac1f03c9..8b1bb9b65 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -1,7 +1,6 @@ package api import ( - "time" "bytes" "encoding/base64" "encoding/json" @@ -12,12 +11,13 @@ import ( "github.com/pquerna/otp/totp" "image/png" "net/http" + "time" ) type EnrollFactorParams struct { - SimpleName string `json:"simple_name"` - FactorType string `json:"factor_type"` - Issuer string `json:"issuer"` + FriendlyName string `json:"friendly_name"` + FactorType string `json:"factor_type"` + Issuer string `json:"issuer"` } type TOTPObject struct { @@ -34,17 +34,17 @@ type EnrollFactorResponse struct { } type ChallengeFactorParams struct { - FactorID string `json:"factor_id"` - FactorSimpleName string `json:"factor_simple_name"` + FactorID string `json:"factor_id"` + FriendlyName string `json:"friendly_name"` } type ChallengeFactorResponse struct { - ID string - CreatedAt string - UpdatedAt string - ExpiresAt string - FactorID string - FactorSimpleName string + ID string + CreatedAt string + UpdatedAt string + ExpiresAt string + FactorID string + FriendlyName string } // RecoveryCodesResponse repreesnts a successful recovery code generation response @@ -182,7 +182,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) factorID := fmt.Sprintf("%s_%s", FACTOR_PREFIX, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.SimpleName, factorID, params.FactorType, key.Secret()) + factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, "disabled", key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -227,16 +227,16 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read EnrollFactor params: %v", err) } factorID := params.FactorID - factorSimpleName := params.FactorSimpleName + friendlyName := params.FriendlyName - if factorID != "" && factorSimpleName != "" { + if factorID != "" && friendlyName != "" { return unprocessableEntityError("Only a FactorID or FactorSimpleName should be provided on signup.") } if factorID != "" { factor, err = models.FindFactorByFactorID(a.db, factorID) - } else if factorSimpleName != "" { - factor, err = models.FindFactorBySimpleName(a.db, factorSimpleName) + } else if friendlyName != "" { + factor, err = models.FindFactorByFriendlyName(a.db, friendlyName) } else { return unprocessableEntityError("Either FactorID or FactorSimpleName should be provided on signup.") } @@ -257,8 +257,9 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return terr } if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ - "factor_id": params.FactorID, - "factor_simple_name": params.FactorSimpleName, + "factor_id": params.FactorID, + "friendly_name": params.FriendlyName, + "factor_status": factor.Status, }); terr != nil { return terr } @@ -271,10 +272,10 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ - ID: challenge.ID, - CreatedAt: creationTime.String(), - ExpiresAt: creationTime.Add(time.Second * CHALLENGE_EXPIRY_DURATION).String(), - FactorID: factor.ID, - FactorSimpleName: factor.SimpleName, + ID: challenge.ID, + CreatedAt: creationTime.String(), + ExpiresAt: creationTime.Add(time.Second * CHALLENGE_EXPIRY_DURATION).String(), + FactorID: factor.ID, + FriendlyName: factor.FriendlyName, }) } diff --git a/api/mfa_test.go b/api/mfa_test.go index ba29ea441..cec2d9386 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -44,7 +44,7 @@ func (ts *MFATestSuite) SetupTest() { u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "phone", "disabled", "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", "disabled", "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") } @@ -110,7 +110,7 @@ func (ts *MFATestSuite) TestChallengeFactor() { cases := []struct { desc string id string - simpleName string + friendlyName string mfaEnabled bool expectedCode int }{ @@ -165,8 +165,8 @@ func (ts *MFATestSuite) TestChallengeFactor() { var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "factor_id": c.id, - "factor_simple_name": c.simpleName, + "factor_id": c.id, + "friendly_name": c.friendlyName, })) req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/mfa/%s/challenge_factor", u.ID), &buffer) diff --git a/models/challenge_test.go b/models/challenge_test.go index 23e4311c6..c7ffb0e64 100644 --- a/models/challenge_test.go +++ b/models/challenge_test.go @@ -41,7 +41,7 @@ func (ts *FactorTestSuite) TestFindChallengesByFactorID() { err = ts.db.Create(u) require.NoError(ts.T(), err) - f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "phone", "topsecret") + f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "totp", "disabled", "topsecret") require.NoError(ts.T(), err) err = ts.db.Create(f) diff --git a/models/factor.go b/models/factor.go index 310541461..8017b7de7 100644 --- a/models/factor.go +++ b/models/factor.go @@ -9,14 +9,14 @@ import ( ) type Factor struct { - UserID uuid.UUID `json:"user_id" db:"user_id"` - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Enabled bool `json:"enabled" db:"enabled"` - SimpleName string `json:"simple_name" db:"simple_name"` - SecretKey string `json:"secret_key" db:"secret_key"` - FactorType string `json:"factor_type" db:"factor_type"` + UserID uuid.UUID `json:"user_id" db:"user_id"` + ID string `json:"id" db:"id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Status string `json:"status" db:"status"` + FriendlyName string `json:"friendly_name" db:"friendly_name"` + SecretKey string `json:"secret_key" db:"secret_key"` + FactorType string `json:"factor_type" db:"factor_type"` } func (Factor) TableName() string { @@ -24,15 +24,15 @@ func (Factor) TableName() string { return tableName } -func NewFactor(user *User, simpleName, id, factorType, secretKey string) (*Factor, error) { +func NewFactor(user *User, friendlyName, id, factorType, status, secretKey string) (*Factor, error) { // TODO: Pass in secret and hash it using bcrypt or equiv factor := &Factor{ - ID: id, - UserID: user.ID, - Enabled: true, - SimpleName: simpleName, - SecretKey: secretKey, - FactorType: factorType, + ID: id, + UserID: user.ID, + Status: status, + FriendlyName: friendlyName, + SecretKey: secretKey, + FactorType: factorType, } return factor, nil } @@ -40,7 +40,7 @@ func NewFactor(user *User, simpleName, id, factorType, secretKey string) (*Facto // FindFactorsByUser returns all factors belonging to a user func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { factors := []*Factor{} - if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { + if err := tx.Q().Where("user_id = ?", user.ID).All(&factors); err != nil { if errors.Cause(err) == sql.ErrNoRows { return factors, nil } @@ -57,8 +57,8 @@ func FindFactorByFactorID(tx *storage.Connection, factorID string) (*Factor, err return factor, nil } -func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, error) { - factor, err := findFactor(tx, "simple_name = ?", simpleName) +func FindFactorByFriendlyName(tx *storage.Connection, friendlyName string) (*Factor, error) { + factor, err := findFactor(tx, "friendly_name = ?", friendlyName) if err != nil { return nil, FactorNotFoundError{} } @@ -66,19 +66,14 @@ func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, } // Change the factor simple name -func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { +func (f *Factor) UpdateFactorFriendlyName(tx *storage.Connection, friendlyName string) error { f.UpdatedAt = time.Now() - return tx.UpdateOnly(f, "simple_name", "updated_at") + return tx.UpdateOnly(f, "friendly_name", "updated_at") } -func (f *Factor) Disable(tx *storage.Connection) error { - f.Enabled = false - return tx.UpdateOnly(f, "enabled") -} - -func (f *Factor) Enable(tx *storage.Connection) error { - f.Enabled = true - return tx.UpdateOnly(f, "enabled") +func (f *Factor) UpdateFactorStatus(tx *storage.Connection, status string) error { + f.Status = status + return tx.UpdateOnly(f, "status") } func findFactor(tx *storage.Connection, query string, args ...interface{}) (*Factor, error) { diff --git a/models/factor_test.go b/models/factor_test.go index 7297ad186..79f3d178d 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -34,28 +34,10 @@ func (ts *FactorTestSuite) SetupTest() { TruncateAll(ts.db) } -func (ts *FactorTestSuite) TestToggleFactorEnabled() { - u, err := NewUser(uuid.Nil, "", "", "", "", nil) - require.NoError(ts.T(), err) - - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "phone", "some-secret") - require.NoError(ts.T(), err) - - require.NoError(ts.T(), f.Disable(ts.db)) - require.Equal(ts.T(), false, f.Enabled) - - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) - - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) - -} - -func (ts *FactorTestSuite) TestFindFactorBySimpleName() { +func (ts *FactorTestSuite) TestFindFactorByFriendlyName() { f := ts.createFactor() - n, err := FindFactorBySimpleName(ts.db, f.SimpleName) + n, err := FindFactorByFriendlyName(ts.db, f.FriendlyName) require.NoError(ts.T(), err) require.Equal(ts.T(), f.ID, n.ID) } @@ -75,7 +57,7 @@ func (ts *FactorTestSuite) createFactor() *Factor { err = ts.db.Create(user) require.NoError(ts.T(), err) - factor, err := NewFactor(user, "asimplename", "factor-which-shall-not-be-named", "phone", "topsecret") + factor, err := NewFactor(user, "asimplename", "factor-which-shall-not-be-named", "totp", "disabled", "topsecret") require.NoError(ts.T(), err) err = ts.db.Create(factor) From 4f2460adef309eda06f5889c65ff01032f608195 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sat, 2 Jul 2022 23:58:14 +0800 Subject: [PATCH 050/180] chore: renaming --- api/mfa.go | 16 +++++----- api/mfa_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 10 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 5636a4df8..e57c3b61a 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -14,9 +14,9 @@ import ( ) type EnrollFactorParams struct { - FactorSimpleName string `json:"factor_simple_name"` - FactorType string `json:"factor_type"` - Issuer string `json:"issuer"` + FriendlyName string `json:"friendly_name"` + FactorType string `json:"factor_type"` + Issuer string `json:"issuer"` } type TOTPObject struct { @@ -129,8 +129,8 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro } func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { - const FACTOR_PREFIX = "factor" - const IMAGE_SIDE_LENGTH = 300 + const factorPrefix = "factor" + const imageSideLength = 300 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -160,16 +160,16 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { var buf bytes.Buffer // Test with QRCode Encode - img, err := key.Image(IMAGE_SIDE_LENGTH, IMAGE_SIDE_LENGTH) + img, err := key.Image(imageSideLength, imageSideLength) png.Encode(&buf, img) if err != nil { return internalServerError("Error generating QR Code image").WithInternalError(err) } qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) - factorID := fmt.Sprintf("%s_%s", FACTOR_PREFIX, crypto.SecureToken()) + factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) // TODO(Joel): Convert this into an Enum - factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, "disabled", key.Secret()) + factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, "disabled", key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } diff --git a/api/mfa_test.go b/api/mfa_test.go index 8986f2d01..a28d32b97 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -79,7 +79,7 @@ func (ts *MFATestSuite) TestMFADisable() { } func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { - const EXPECTED_NUM_OF_RECOVERY_CODES = 8 + const expectedNumOfRecoveryCodes = 8 user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) @@ -98,5 +98,83 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) recoveryCodes := data["recovery_codes"].([]interface{}) - require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, len(recoveryCodes)) + require.Equal(ts.T(), expectedNumOfRecoveryCodes, len(recoveryCodes)) +} + +func (ts *MFATestSuite) TestEnrollFactor() { + // var cases = []struct { + // desc string + // newPassword string + // nonce string + // requireReauthentication bool + // expected expected + // }{ + // { + // "Valid password length", + // "newpassword", + // "", + // false, + // expected{code: http.StatusOK, isAuthenticated: true}, + // }, + // { + // "Invalid password length", + // "", + // "", + // false, + // expected{code: http.StatusUnprocessableEntity, isAuthenticated: false}, + // }, + // { + // "No reauthentication provided", + // "newpassword123", + // "", + // true, + // expected{code: http.StatusUnauthorized, isAuthenticated: false}, + // }, + // { + // "Invalid nonce", + // "newpassword123", + // "123456", + // true, + // expected{code: http.StatusBadRequest, isAuthenticated: false}, + // }, + // } + // Check the return type, QR Code representation should be accurate + // + // for _, c := range cases { + // ts.Run(c.desc, func() { + // ts.Config.Security.UpdatePasswordRequireReauthentication = c.requireReauthentication + // var buffer bytes.Buffer + // require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"password": c.newPassword, "nonce": c.nonce})) + + // req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) + // req.Header.Set("Content-Type", "application/json") + + // token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + // require.NoError(ts.T(), err) + // req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + // // Setup response recorder + // w := httptest.NewRecorder() + // ts.API.handler.ServeHTTP(w, req) + // require.Equal(ts.T(), c.expected.code, w.Code) + + // // Request body + // u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + // require.NoError(ts.T(), err) + + // require.Equal(ts.T(), c.expected.isAuthenticated, u.Authenticate(c.newPassword)) + // }) + // } + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + ts.Require().NoError(err) + require.NoError(ts.T(), user.EnableMFA(ts.API.db)) + + token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/enroll_factor", user.ID), nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) } From 1ae17b7164acff841b37ce1533623f9c9f4862a1 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 3 Jul 2022 14:26:07 +0800 Subject: [PATCH 051/180] tests: refactor and add initial enroll factor tests --- api/mfa.go | 3 +- api/mfa_test.go | 131 +++++++++++++++++++++--------------------------- 2 files changed, 60 insertions(+), 74 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index e57c3b61a..ae5685722 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -149,9 +149,10 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") } + // TODO(Joel): Review this portion when email is no longer a primary key key, err := totp.Generate(totp.GenerateOpts{ Issuer: params.Issuer, - AccountName: params.Issuer, + AccountName: user.GetEmail(), }) if err != nil { diff --git a/api/mfa_test.go b/api/mfa_test.go index a28d32b97..38d9cf123 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -102,79 +103,63 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { } func (ts *MFATestSuite) TestEnrollFactor() { - // var cases = []struct { - // desc string - // newPassword string - // nonce string - // requireReauthentication bool - // expected expected - // }{ - // { - // "Valid password length", - // "newpassword", - // "", - // false, - // expected{code: http.StatusOK, isAuthenticated: true}, - // }, - // { - // "Invalid password length", - // "", - // "", - // false, - // expected{code: http.StatusUnprocessableEntity, isAuthenticated: false}, - // }, - // { - // "No reauthentication provided", - // "newpassword123", - // "", - // true, - // expected{code: http.StatusUnauthorized, isAuthenticated: false}, - // }, - // { - // "Invalid nonce", - // "newpassword123", - // "123456", - // true, - // expected{code: http.StatusBadRequest, isAuthenticated: false}, - // }, - // } + var cases = []struct { + desc string + FriendlyName string + FactorType string + Issuer string + MFAEnabled bool + expectedCode int + }{ + { + "TOTP: MFA is disabled", + "", + "totp", + "supabase.com", + false, + http.StatusForbidden, + }, + { + "TOTP: Factor has friendly name", + "bob", + "totp", + "supabase.com", + true, + http.StatusOK, + }, + { + "TOTP: Without simple name", + "", + "totp", + "supabase.com", + true, + http.StatusOK, + }, + } // Check the return type, QR Code representation should be accurate - // - // for _, c := range cases { - // ts.Run(c.desc, func() { - // ts.Config.Security.UpdatePasswordRequireReauthentication = c.requireReauthentication - // var buffer bytes.Buffer - // require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"password": c.newPassword, "nonce": c.nonce})) - - // req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) - // req.Header.Set("Content-Type", "application/json") - - // token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - // require.NoError(ts.T(), err) - // req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - - // // Setup response recorder - // w := httptest.NewRecorder() - // ts.API.handler.ServeHTTP(w, req) - // require.Equal(ts.T(), c.expected.code, w.Code) - - // // Request body - // u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - // require.NoError(ts.T(), err) - - // require.Equal(ts.T(), c.expected.isAuthenticated, u.Authenticate(c.newPassword)) - // }) - // } - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - ts.Require().NoError(err) - require.NoError(ts.T(), user.EnableMFA(ts.API.db)) - - token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err) + for _, c := range cases { + ts.Run(c.desc, func() { + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"friendly_name": c.FriendlyName, "factor_type": c.FactorType, "issuer": c.Issuer})) + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + ts.Require().NoError(err) + require.NoError(ts.T(), user.EnableMFA(ts.API.db)) + + token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/enroll_factor", user.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Content-Type", "application/json") + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + // Should be able to convert the returned string into a base64 image + // FactorType returned should be the same + // Factor is disabled + // DB level checks + // If simple name is pased in it should be present + }) + } - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/enroll_factor", user.ID), nil) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) } From 006d4785a7c5447195a97ec2d12317b515d80173 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 3 Jul 2022 20:33:11 +0800 Subject: [PATCH 052/180] fix: add associations from factor to user --- api/mfa_test.go | 13 +++++++------ api/signup_test.go | 2 +- models/challenge.go | 42 ++++++++++++++++++++++++++++++++++++++++++ models/factor.go | 9 +++++---- models/user.go | 1 + 5 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 models/challenge.go diff --git a/api/mfa_test.go b/api/mfa_test.go index 38d9cf123..a6b1048a9 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -136,7 +136,6 @@ func (ts *MFATestSuite) TestEnrollFactor() { http.StatusOK, }, } - // Check the return type, QR Code representation should be accurate for _, c := range cases { ts.Run(c.desc, func() { var buffer bytes.Buffer @@ -154,11 +153,13 @@ func (ts *MFATestSuite) TestEnrollFactor() { req.Header.Set("Content-Type", "application/json") ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) - // Should be able to convert the returned string into a base64 image - // FactorType returned should be the same - // Factor is disabled - // DB level checks - // If simple name is pased in it should be present + factors, err := models.FindFactorsByUser(ts.API.db, user) + ts.Require().NoError(err) + latestFactor := factors[len(factors)-1] + require.Equal(ts.T(), "disabled", latestFactor.Status) + if c.FriendlyName != "" { + require.Equal(ts.T(), c.FriendlyName, latestFactor.FriendlyName) + } }) } diff --git a/api/signup_test.go b/api/signup_test.go index a5327dece..96e6c67b3 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -78,7 +78,7 @@ func (ts *SignupTestSuite) TestSignup() { } func (ts *SignupTestSuite) TestWebhookTriggered() { - const numUserFields = 11 + const numUserFields = 12 var callCount int require := ts.Require() assert := ts.Assert() diff --git a/models/challenge.go b/models/challenge.go new file mode 100644 index 000000000..92c366e63 --- /dev/null +++ b/models/challenge.go @@ -0,0 +1,42 @@ +package models + +import ( + "database/sql" + "fmt" + "github.com/netlify/gotrue/crypto" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "time" +) + +type Challenge struct { + ID string `json:"challenge_id" db:"id"` + FactorID string `json:"factor_id" db:"factor_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` +} + +func (Challenge) TableName() string { + tableName := "mfa_challenges" + return tableName +} + +const CHALLENGE_PREFIX = "challenge" + +func NewChallenge(factor *Factor) (*Challenge, error) { + challenge := &Challenge{ + ID: fmt.Sprintf("%s_%s", CHALLENGE_PREFIX, crypto.SecureToken()), + FactorID: factor.ID, + } + return challenge, nil +} + +func FindChallengesByFactorID(tx *storage.Connection, factorID string) ([]*Challenge, error) { + challenges := []*Challenge{} + if err := tx.Q().Where("factor_id = ?", factorID).All(&challenges); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return challenges, nil + } + return nil, errors.Wrap(err, "Error finding MFA Challenges for factor") + } + return challenges, nil +} diff --git a/models/factor.go b/models/factor.go index 52e10cbc0..01b896f89 100644 --- a/models/factor.go +++ b/models/factor.go @@ -9,13 +9,14 @@ import ( ) type Factor struct { - UserID uuid.UUID `json: "user_id" db:"user_id"` ID string `json:"id" db:"id"` + User User `belongs_to:"user"` + UserID uuid.UUID `json:"user_id" db:"user_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` Status string `json:"status" db:"status"` FriendlyName string `json:"friendly_name" db:"friendly_name"` - SecretKey string `json:'-' db:'secret_key'` + SecretKey string `json:"-" db:"secret_key"` FactorType string `json:"factor_type" db:"factor_type"` } @@ -26,8 +27,8 @@ func (Factor) TableName() string { func NewFactor(user *User, friendlyName, id, factorType, status, secretKey string) (*Factor, error) { factor := &Factor{ - ID: id, UserID: user.ID, + ID: id, Status: status, FriendlyName: friendlyName, SecretKey: secretKey, @@ -39,7 +40,7 @@ func NewFactor(user *User, friendlyName, id, factorType, status, secretKey strin // FindFactorsByUser returns all factors belonging to a user func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { factors := []*Factor{} - if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { + if err := tx.Q().Where("user_id = ?", user.ID).Order("created_at asc").All(&factors); err != nil { if errors.Cause(err) == sql.ErrNoRows { return factors, nil } diff --git a/models/user.go b/models/user.go index 1dea6d8e4..5d886f80e 100644 --- a/models/user.go +++ b/models/user.go @@ -61,6 +61,7 @@ type User struct { IsSuperAdmin bool `json:"-" db:"is_super_admin"` Identities []Identity `json:"identities" has_many:"identities"` + Factors []Factor `json:"factors" has_many:"factors"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` From 6edf167158fb93a2b660ebf1a6d6abe32e2235c4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 4 Jul 2022 23:56:44 +0800 Subject: [PATCH 053/180] refactor: cleanup --- api/mfa.go | 3 --- api/mfa_test.go | 2 -- models/challenge.go | 4 ++-- models/factor.go | 4 ++-- models/factor_test.go | 9 --------- 5 files changed, 4 insertions(+), 18 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index ae5685722..72d8d18ac 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -159,8 +159,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Error generating QR Code secret key").WithInternalError(err) } var buf bytes.Buffer - - // Test with QRCode Encode img, err := key.Image(imageSideLength, imageSideLength) png.Encode(&buf, img) if err != nil { @@ -174,7 +172,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } - terr = a.db.Transaction(func(tx *storage.Connection) error { if terr = tx.Create(factor); terr != nil { return terr diff --git a/api/mfa_test.go b/api/mfa_test.go index a6b1048a9..0f6cf5ae2 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -143,10 +143,8 @@ func (ts *MFATestSuite) TestEnrollFactor() { user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) require.NoError(ts.T(), user.EnableMFA(ts.API.db)) - token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) - w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/enroll_factor", user.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) diff --git a/models/challenge.go b/models/challenge.go index 92c366e63..005b58789 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -20,11 +20,11 @@ func (Challenge) TableName() string { return tableName } -const CHALLENGE_PREFIX = "challenge" +const ChallengePrefix = "challenge" func NewChallenge(factor *Factor) (*Challenge, error) { challenge := &Challenge{ - ID: fmt.Sprintf("%s_%s", CHALLENGE_PREFIX, crypto.SecureToken()), + ID: fmt.Sprintf("%s_%s", ChallengePrefix, crypto.SecureToken()), FactorID: factor.ID, } return challenge, nil diff --git a/models/factor.go b/models/factor.go index 01b896f89..ba1b62923 100644 --- a/models/factor.go +++ b/models/factor.go @@ -37,7 +37,7 @@ func NewFactor(user *User, friendlyName, id, factorType, status, secretKey strin return factor, nil } -// FindFactorsByUser returns all factors belonging to a user +// FindFactorsByUser returns all factors belonging to a user ordered by timestamp func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { factors := []*Factor{} if err := tx.Q().Where("user_id = ?", user.ID).Order("created_at asc").All(&factors); err != nil { @@ -55,7 +55,7 @@ func (f *Factor) UpdateFriendlyName(tx *storage.Connection, friendlyName string) return tx.UpdateOnly(f, "friendly_name", "updated_at") } -//Change the factor status +// Change the factor status func (f *Factor) UpdateStatus(tx *storage.Connection, status string) error { f.Status = status return tx.UpdateOnly(f, "status", "updated_at") diff --git a/models/factor_test.go b/models/factor_test.go index a7a73d9e7..07c0d5bd1 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -18,15 +18,12 @@ type FactorTestSuite struct { func TestFactor(t *testing.T) { globalConfig, err := conf.LoadGlobal(modelsTestConfig) require.NoError(t, err) - conn, err := test.SetupDBConnection(globalConfig) require.NoError(t, err) - ts := &FactorTestSuite{ db: conn, } defer ts.db.Close() - suite.Run(t, ts) } @@ -38,24 +35,18 @@ func (ts *FactorTestSuite) TestUpdateStatus() { newFactorStatus := "verified" u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "disabled", "") require.NoError(ts.T(), err) - require.NoError(ts.T(), f.UpdateStatus(ts.db, newFactorStatus)) require.Equal(ts.T(), newFactorStatus, f.Status) } func (ts *FactorTestSuite) TestUpdateFriendlyName() { newSimpleName := "newFactorName" - u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "disabled", "") require.NoError(ts.T(), err) - require.NoError(ts.T(), f.UpdateFriendlyName(ts.db, newSimpleName)) require.Equal(ts.T(), newSimpleName, f.FriendlyName) - } From ea4d3466e12a9143f23ff798f5f2de5f9585d67e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 5 Jul 2022 00:07:05 +0800 Subject: [PATCH 054/180] refactor: cleanup magic strings --- api/mfa.go | 13 ++----------- models/factor.go | 4 ++++ models/factor_test.go | 6 +++--- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 72d8d18ac..469ed6087 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -32,8 +32,6 @@ type EnrollFactorResponse struct { TOTP TOTPObject } -// RecoveryCodesResponse repreesnts a successful Recovery code generation response - type RecoveryCodesResponse struct { RecoveryCodes []string `json:"recovery_codes"` } @@ -137,24 +135,20 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if !user.MFAEnabled { return MFANotEnabledError } - params := &EnrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) err := jsonDecoder.Decode(params) if err != nil { return badRequestError("Could not read EnrollFactor params: %v", err) } - if (params.FactorType != "totp") && (params.FactorType != "webauthn") { return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") } - // TODO(Joel): Review this portion when email is no longer a primary key key, err := totp.Generate(totp.GenerateOpts{ Issuer: params.Issuer, AccountName: user.GetEmail(), }) - if err != nil { return internalServerError("Error generating QR Code secret key").WithInternalError(err) } @@ -166,9 +160,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) - - // TODO(Joel): Convert this into an Enum - factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, "disabled", key.Secret()) + // TODO(Joel): Convert constants into an Enum in future + factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, models.FactorDisabledState, key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -179,10 +172,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(tx, instanceID, user, models.EnrollFactorAction, r.RemoteAddr, nil); terr != nil { return terr } - return nil }) - return sendJSON(w, http.StatusOK, &EnrollFactorResponse{ ID: factor.ID, Type: factor.FactorType, diff --git a/models/factor.go b/models/factor.go index ba1b62923..42f2c240a 100644 --- a/models/factor.go +++ b/models/factor.go @@ -8,6 +8,10 @@ import ( "time" ) +const FactorDisabledState = "disabled" +const FactorUnverifiedState = "unverified" +const FactorVerifiedState = "verified" + type Factor struct { ID string `json:"id" db:"id"` User User `belongs_to:"user"` diff --git a/models/factor_test.go b/models/factor_test.go index 07c0d5bd1..f3e8a675e 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -32,10 +32,10 @@ func (ts *FactorTestSuite) SetupTest() { } func (ts *FactorTestSuite) TestUpdateStatus() { - newFactorStatus := "verified" + newFactorStatus := FactorVerifiedState u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "disabled", "") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorDisabledState, "") require.NoError(ts.T(), err) require.NoError(ts.T(), f.UpdateStatus(ts.db, newFactorStatus)) require.Equal(ts.T(), newFactorStatus, f.Status) @@ -45,7 +45,7 @@ func (ts *FactorTestSuite) TestUpdateFriendlyName() { newSimpleName := "newFactorName" u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", "disabled", "") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorDisabledState, "") require.NoError(ts.T(), err) require.NoError(ts.T(), f.UpdateFriendlyName(ts.db, newSimpleName)) require.Equal(ts.T(), newSimpleName, f.FriendlyName) From ccdc78f3f1311d377948fb6d6659d9d41e0b3131 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 5 Jul 2022 01:53:20 +0800 Subject: [PATCH 055/180] refactor: remove newlines --- api/mfa.go | 5 ++--- api/mfa_test.go | 4 +--- models/audit_log_entry.go | 6 +++--- models/challenge.go | 1 - models/challenge_test.go | 11 +---------- models/factor.go | 4 +--- models/factor_test.go | 6 +----- 7 files changed, 9 insertions(+), 28 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 184376d7b..4a4703109 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -201,7 +201,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { }) } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { - const CHALLENGE_EXPIRY_DURATION = 300 + const challengeExpiryDuration = 300 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -265,9 +265,8 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, CreatedAt: creationTime.String(), - ExpiresAt: creationTime.Add(time.Second * CHALLENGE_EXPIRY_DURATION).String(), + ExpiresAt: creationTime.Add(time.Second * challengeExpiryDuration).String(), FactorID: factor.ID, FriendlyName: factor.FriendlyName, }) } - diff --git a/api/mfa_test.go b/api/mfa_test.go index c2a4b9116..d5cc7e314 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -44,7 +44,7 @@ func (ts *MFATestSuite) SetupTest() { u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", "disabled", "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") } @@ -167,7 +167,6 @@ func (ts *MFATestSuite) TestEnrollFactor() { } func (ts *MFATestSuite) TestChallengeFactor() { - cases := []struct { desc string id string @@ -211,7 +210,6 @@ func (ts *MFATestSuite) TestChallengeFactor() { http.StatusUnprocessableEntity, }, } - for _, c := range cases { ts.Run(c.desc, func() { u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 272c82b62..88822ddb7 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -32,11 +32,11 @@ const ( EnrollFactorAction AuditAction = "factor_enrolled" CreateChallengeAction AuditAction = "challenge_created" - account auditLogType = "account" team auditLogType = "team" token auditLogType = "token" user auditLogType = "user" + factor auditLogType = "factor" ) var actionLogTypeMap = map[AuditAction]auditLogType{ @@ -53,8 +53,8 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ UserConfirmationRequestedAction: user, UserRepeatedSignUpAction: user, GenerateRecoveryCodesAction: user, - EnrollFactorAction: user, - CreateChallengeAction: user, + EnrollFactorAction: factor, + CreateChallengeAction: factor, } // AuditLogEntry is the database model for audit log entries. diff --git a/models/challenge.go b/models/challenge.go index 8ef9d5a6c..005b58789 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -20,7 +20,6 @@ func (Challenge) TableName() string { return tableName } - const ChallengePrefix = "challenge" func NewChallenge(factor *Factor) (*Challenge, error) { diff --git a/models/challenge_test.go b/models/challenge_test.go index c7ffb0e64..97ed3d2e5 100644 --- a/models/challenge_test.go +++ b/models/challenge_test.go @@ -18,15 +18,12 @@ type ChallengeTestSuite struct { func TestChallenge(t *testing.T) { globalConfig, err := conf.LoadGlobal(modelsTestConfig) require.NoError(t, err) - conn, err := test.SetupDBConnection(globalConfig) require.NoError(t, err) - ts := &ChallengeTestSuite{ db: conn, } defer ts.db.Close() - suite.Run(t, ts) } @@ -37,22 +34,16 @@ func (ts *ChallengeTestSuite) SetupTest() { func (ts *FactorTestSuite) TestFindChallengesByFactorID() { u, err := NewUser(uuid.Nil, "", "genericemail@gmail.com", "secret", "test", nil) require.NoError(ts.T(), err) - err = ts.db.Create(u) require.NoError(ts.T(), err) - - f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "totp", "disabled", "topsecret") + f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "totp", FactorDisabledState, "topsecret") require.NoError(ts.T(), err) - err = ts.db.Create(f) require.NoError(ts.T(), err) - c, err := NewChallenge(f) require.NoError(ts.T(), err) - err = ts.db.Create(c) require.NoError(ts.T(), err) - n, err := FindChallengesByFactorID(ts.db, c.FactorID) require.NoError(ts.T(), err) require.Len(ts.T(), n, 1) diff --git a/models/factor.go b/models/factor.go index e6b1d8589..0901fe71a 100644 --- a/models/factor.go +++ b/models/factor.go @@ -8,7 +8,6 @@ import ( "time" ) - const FactorDisabledState = "disabled" const FactorUnverifiedState = "unverified" const FactorVerifiedState = "verified" @@ -42,7 +41,6 @@ func NewFactor(user *User, friendlyName, id, factorType, status, secretKey strin return factor, nil } - // FindFactorsByUser returns all factors belonging to a user ordered by timestamp func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { factors := []*Factor{} @@ -71,7 +69,6 @@ func FindFactorByFriendlyName(tx *storage.Connection, friendlyName string) (*Fac return factor, nil } - func findFactor(tx *storage.Connection, query string, args ...interface{}) (*Factor, error) { obj := &Factor{} if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil { @@ -83,6 +80,7 @@ func findFactor(tx *storage.Connection, query string, args ...interface{}) (*Fac return obj, nil } + // Change the friendly name func (f *Factor) UpdateFriendlyName(tx *storage.Connection, friendlyName string) error { f.FriendlyName = friendlyName diff --git a/models/factor_test.go b/models/factor_test.go index 4cc34bce9..f1498887b 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -41,7 +41,6 @@ func (ts *FactorTestSuite) TestFindFactorByFriendlyName() { func (ts *FactorTestSuite) TestFindFactorByFactorID() { f := ts.createFactor() - n, err := FindFactorByFactorID(ts.db, f.ID) require.NoError(ts.T(), err) require.Equal(ts.T(), f.ID, n.ID) @@ -50,18 +49,15 @@ func (ts *FactorTestSuite) TestFindFactorByFactorID() { func (ts *FactorTestSuite) createFactor() *Factor { user, err := NewUser(uuid.Nil, "", "agenericemail@gmail.com", "secret", "test", nil) require.NoError(ts.T(), err) - err = ts.db.Create(user) require.NoError(ts.T(), err) - factor, err := NewFactor(user, "asimplename", "factor-which-shall-not-be-named", "totp", "disabled", "topsecret") require.NoError(ts.T(), err) - err = ts.db.Create(factor) require.NoError(ts.T(), err) return factor - } +} func (ts *FactorTestSuite) TestUpdateStatus() { newFactorStatus := FactorVerifiedState u, err := NewUser(uuid.Nil, "", "", "", "", nil) From 2bd0dfc6092f010fa70fa308954d3cde65029ea0 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 5 Jul 2022 20:25:16 +0800 Subject: [PATCH 056/180] chore: introduce new factor type --- api/mfa.go | 6 +----- models/audit_log_entry.go | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 35af031c3..4a63ab2c7 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -298,7 +298,6 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { - // TODO: Joel We need to attach a rate limiter to this so it doesn't get fuzzed var err error ctx := r.Context() user := getUser(ctx) @@ -312,7 +311,6 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { if err != nil { return badRequestError("Could not read VerifyFactor params: %v", err) } - factor, err := models.FindFactorByChallengeID(a.db, params.ChallengeID) if err != nil { if models.IsNotFoundError(err) { @@ -320,10 +318,9 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return internalServerError("Database error finding factor").WithInternalError(err) } - // No unhashing it err = a.db.Transaction(func(tx *storage.Connection) error { - if err = models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ + if err = models.NewAuditLogEntry(tx, instanceID, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ "factor_id": factor.ID, "challenge_id": params.ChallengeID, }); err != nil { @@ -331,7 +328,6 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return nil }) - // valid := totp.Validate(params.Code, factor.SecretKey) if valid != true { return unauthorizedError("Invalid code entered") diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 7e98406bf..ed642f850 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -31,6 +31,7 @@ const ( GenerateRecoveryCodesAction AuditAction = "generate_recovery_codes" EnrollFactorAction AuditAction = "factor_enrolled" CreateChallengeAction AuditAction = "challenge_created" + VerifyFactorAction AuditAction = "verification_attempted" account auditLogType = "account" team auditLogType = "team" @@ -54,6 +55,7 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ GenerateRecoveryCodesAction: user, EnrollFactorAction: user, CreateChallengeAction: user, + VerifyFactorAction: user, } // AuditLogEntry is the database model for audit log entries. From 60c800b34e8bdfe84d1cce65c7483574ea9f76a7 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 5 Jul 2022 23:02:29 +0800 Subject: [PATCH 057/180] chore: backpatch naming --- api/mfa.go | 40 ++++++++++++++----- models/challenge.go | 34 ++++++++++++++-- models/errors.go | 9 +++++ models/factor.go | 93 ++++++++++++++++++++----------------------- models/factor_test.go | 15 +------ 5 files changed, 113 insertions(+), 78 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index c52798c3e..960b88811 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -11,6 +11,7 @@ import ( "github.com/pquerna/otp/totp" "image/png" "net/http" + "time" ) type EnrollFactorParams struct { @@ -33,8 +34,8 @@ type EnrollFactorResponse struct { } type ChallengeFactorParams struct { - FactorID string - FactorSimpleName string + FactorID string `json:"factor_id"` + FriendlyName string `json:"friendly_name"` } type VerifyFactorParams struct { @@ -56,7 +57,6 @@ type VerifyFactorResponse struct { Success string } - // RecoveryCodesResponse repreesnts a successful recovery code generation response type RecoveryCodesResponse struct { RecoveryCodes []string `json:"recovery_codes"` @@ -193,7 +193,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) factorID := fmt.Sprintf("%s_%s", FACTOR_PREFIX, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, key.Secret()) + factor, terr := models.NewFactor(user, params.FactorSimpleName, factorID, params.FactorType, "disabled", key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -238,16 +238,16 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read EnrollFactor params: %v", err) } factorID := params.FactorID - factorSimpleName := params.FactorSimpleName + friendlyName := params.FriendlyName - if factorID != "" && factorSimpleName != "" { + if factorID != "" && friendlyName != "" { return unprocessableEntityError("Only a FactorID or FactorSimpleName should be provided on signup.") } if factorID != "" { - factor, err = models.FindFactorByFactorID(a.db, factorID) - } else if params.FactorSimpleName != "" { - factor, err = models.FindFactorBySimpleName(a.db, factorSimpleName) + factor, err = models.FindFactorByID(a.db, factorID) + } else if params.FriendlyName != "" { + factor, err = models.FindFactorByFriendlyName(a.db, friendlyName) } if err != nil { if models.IsNotFoundError(err) { @@ -267,7 +267,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ "factor_id": params.FactorID, - "factor_simple_name": params.FactorSimpleName, + "factor_simple_name": params.FriendlyName, }); terr != nil { return terr } @@ -309,14 +309,32 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return internalServerError("Database error finding factor").WithInternalError(err) } - err = a.db.Transaction(func(tx *storage.Connection) error { + challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) + if err != nil { + if models.IsNotFoundError(err) { + return notFoundError(err.Error()) + } + return internalServerError("Database error finding Challenge").WithInternalError(err) + + } + err = a.db.Transaction(func(tx *storage.Connection) error { if err = models.NewAuditLogEntry(tx, instanceID, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ "factor_id": factor.ID, "challenge_id": params.ChallengeID, }); err != nil { return err } + if err = challenge.Verify(a.db); err != nil { + return err + } + // TODO: Joel -- substitute this with status constants once main branch is merged in + if factor.Status != "verified" { + if err = factor.UpdateStatus(a.db, "verified"); err != nil { + return err + } + } + return nil }) valid := totp.Validate(params.Code, factor.SecretKey) diff --git a/models/challenge.go b/models/challenge.go index 588977a26..5ee7e90b1 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -10,9 +10,10 @@ import ( ) type Challenge struct { - ID string `json:"challenge_id" db:"id"` - FactorID string `json:"factor_id" db:"factor_id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + ID string `json:"challenge_id" db:"id"` + FactorID string `json:"factor_id" db:"factor_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + VerifiedAt *time.Time `json:"verified_at" db:"verified_at"` } const CHALLENGE_PREFIX = "challenge" @@ -30,6 +31,14 @@ func NewChallenge(factorID string) (*Challenge, error) { return challenge, nil } +func FindChallengeByChallengeID(tx *storage.Connection, challengeID string) (*Challenge, error) { + challenge, err := findChallenge(tx, "id = ?", challengeID) + if err != nil { + return nil, ChallengeNotFoundError{} + } + return challenge, nil +} + func FindChallengesByFactorID(tx *storage.Connection, factorID string) ([]*Challenge, error) { challenges := []*Challenge{} if err := tx.Q().Where("factor_id = ?", factorID, true).All(&challenges); err != nil { @@ -40,3 +49,22 @@ func FindChallengesByFactorID(tx *storage.Connection, factorID string) ([]*Chall } return challenges, nil } + +// Update the verification timestamp +func (f *Challenge) Verify(tx *storage.Connection) error { + now := time.Now() + f.VerifiedAt = &now + return tx.UpdateOnly(f, "verifiedAt") +} + +func findChallenge(tx *storage.Connection, query string, args ...interface{}) (*Challenge, error) { + obj := &Challenge{} + if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, ChallengeNotFoundError{} + } + return nil, errors.Wrap(err, "error finding challenge") + } + + return obj, nil +} diff --git a/models/errors.go b/models/errors.go index 054d8fa00..058bc2708 100644 --- a/models/errors.go +++ b/models/errors.go @@ -15,6 +15,8 @@ func IsNotFoundError(err error) bool { return true case IdentityNotFoundError: return true + case ChallengeNotFoundError: + return true } return false } @@ -61,6 +63,13 @@ func (e FactorNotFoundError) Error() string { return "Factor not found" } +// ChallengeNotFoundError represents when a user is not found. +type ChallengeNotFoundError struct{} + +func (e ChallengeNotFoundError) Error() string { + return "Challenge not found" +} + type TotpSecretNotFoundError struct{} func (e TotpSecretNotFoundError) Error() string { diff --git a/models/factor.go b/models/factor.go index c32a2a218..51fe393ad 100644 --- a/models/factor.go +++ b/models/factor.go @@ -9,14 +9,15 @@ import ( ) type Factor struct { - UserID uuid.UUID `json:"-" db:"user_id"` - ID string `json:"id" db:"id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - Enabled bool `json:"enabled" db:"enabled"` - FactorSimpleName string `json:"simple_name" db:"simple_name"` - SecretKey string `json:"-" db:"secret_key"` - FactorType string `json:"factor_type" db:"factor_type"` + ID string `json:"id" db:"id"` + User User `belongs_to:"user"` + UserID uuid.UUID `json:"user_id" db:"user_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + Status string `json:"status" db:"status"` + FriendlyName string `json:"friendly_name" db:"friendly_name"` + SecretKey string `json:"-" db:"secret_key"` + FactorType string `json:"factor_type" db:"factor_type"` } func (Factor) TableName() string { @@ -24,15 +25,14 @@ func (Factor) TableName() string { return tableName } -func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) (*Factor, error) { - // TODO: Pass in secret and hash it using bcrypt or equiv +func NewFactor(user *User, friendlyName, id, factorType, status, secretKey string) (*Factor, error) { factor := &Factor{ - ID: id, - UserID: user.ID, - Enabled: true, - FactorSimpleName: factorSimpleName, - SecretKey: secretKey, - FactorType: factorType, + UserID: user.ID, + ID: id, + Status: status, + FriendlyName: friendlyName, + SecretKey: secretKey, + FactorType: factorType, } return factor, nil } @@ -40,7 +40,7 @@ func NewFactor(user *User, factorSimpleName, id, factorType, secretKey string) ( // FindFactorsByUser returns all factors belonging to a user func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { factors := []*Factor{} - if err := tx.Q().Where("user_id = ?", user.ID, true).All(&factors); err != nil { + if err := tx.Q().Where("user_id = ?", user.ID).Order("created_at asc").All(&factors); err != nil { if errors.Cause(err) == sql.ErrNoRows { return factors, nil } @@ -49,20 +49,21 @@ func FindFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { return factors, nil } -func FindFactorByFactorID(tx *storage.Connection, factorID string) (*Factor, error) { - factor, err := findFactor(tx, "id = ?", factorID) - if err != nil { - return nil, FactorNotFoundError{} - } - return factor, nil +// FindFactorByID finds a factor matching the provided ID. +func FindFactorByID(tx *storage.Connection, factorID string) (*Factor, error) { + return findFactor(tx, "id = ?", factorID) } -func FindFactorBySimpleName(tx *storage.Connection, simpleName string) (*Factor, error) { - factor, err := findFactor(tx, "factor_simple_name = ?", simpleName) - if err != nil { - return nil, FactorNotFoundError{} +func findFactor(tx *storage.Connection, query string, args ...interface{}) (*Factor, error) { + obj := &Factor{} + if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, FactorNotFoundError{} + } + return nil, errors.Wrap(err, "error finding factor") } - return factor, nil + + return obj, nil } func FindFactorByChallengeID(tx *storage.Connection, challengeID string) (*Factor, error) { @@ -76,30 +77,22 @@ func FindFactorByChallengeID(tx *storage.Connection, challengeID string) (*Facto return factor, nil } -// Change the factor simple name -func (f *Factor) UpdateFactorSimpleName(tx *storage.Connection) error { - f.UpdatedAt = time.Now() - return tx.UpdateOnly(f, "factor_simple_name", "updated_at") -} - -func (f *Factor) Disable(tx *storage.Connection) error { - f.Enabled = false - return tx.UpdateOnly(f, "enabled") +func FindFactorByFriendlyName(tx *storage.Connection, friendlyName string) (*Factor, error) { + factor, err := findFactor(tx, "friendly_name = ?", friendlyName) + if err != nil { + return nil, FactorNotFoundError{} + } + return factor, nil } -func (f *Factor) Enable(tx *storage.Connection) error { - f.Enabled = true - return tx.UpdateOnly(f, "enabled") +// Change the friendly name +func (f *Factor) UpdateFriendlyName(tx *storage.Connection, friendlyName string) error { + f.FriendlyName = friendlyName + return tx.UpdateOnly(f, "friendly_name", "updated_at") } -func findFactor(tx *storage.Connection, query string, args ...interface{}) (*Factor, error) { - obj := &Factor{} - if err := tx.Eager().Q().Where(query, args...).First(obj); err != nil { - if errors.Cause(err) == sql.ErrNoRows { - return nil, FactorNotFoundError{} - } - return nil, errors.Wrap(err, "error finding factor") - } - - return obj, nil +//Change the factor status +func (f *Factor) UpdateStatus(tx *storage.Connection, status string) error { + f.Status = status + return tx.UpdateOnly(f, "status", "updated_at") } diff --git a/models/factor_test.go b/models/factor_test.go index 2ac8ff72f..3ae8a25cf 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -46,19 +46,6 @@ func (ts *FactorTestSuite) SetupTest() { TruncateAll(ts.db) } -func (ts *FactorTestSuite) TestToggleFactorEnabled() { - f := ts.createFactor() - require.NoError(ts.T(), f.Disable(ts.db)) - require.Equal(ts.T(), false, f.Enabled) - - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) - - require.NoError(ts.T(), f.Enable(ts.db)) - require.Equal(ts.T(), true, f.Enabled) - -} - func (ts *FactorTestSuite) createFactor() *Factor { u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) @@ -66,7 +53,7 @@ func (ts *FactorTestSuite) createFactor() *Factor { err = ts.db.Create(u) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "phone", "supersecretkey") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "supersecretkey") require.NoError(ts.T(), err) err = ts.db.Create(f) From 141a71f608b7e40036e2596a60e0da14d913aa6e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 6 Jul 2022 01:01:20 +0800 Subject: [PATCH 058/180] test: update verify factor test --- api/mfa_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index 8986f2d01..e66402238 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -1,6 +1,8 @@ package api import ( + "bytes" + "encoding/base32" "encoding/json" "fmt" "net/http" @@ -11,6 +13,7 @@ import ( "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" + "github.com/pquerna/otp/totp" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -43,6 +46,7 @@ func (ts *MFATestSuite) SetupTest() { u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") + } func (ts *MFATestSuite) TestMFAEnable() { @@ -80,7 +84,6 @@ func (ts *MFATestSuite) TestMFADisable() { func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { const EXPECTED_NUM_OF_RECOVERY_CODES = 8 - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) require.NoError(ts.T(), user.EnableMFA(ts.API.db)) @@ -100,3 +103,42 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { recoveryCodes := data["recovery_codes"].([]interface{}) require.Equal(ts.T(), EXPECTED_NUM_OF_RECOVERY_CODES, len(recoveryCodes)) } + +func (ts *MFATestSuite) TestMFAVerifyFactor() { + u, err := models.NewUser(ts.instanceID, "1234567891", "test123@example.com", "password", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error creating test user model") + require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") + + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", "disabled", "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + + c, err := models.NewChallenge(f.ID) + require.NoError(ts.T(), err, "Error creating test Challenge model") + require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") + // TOTP library takes in base32 string + secret := base32.StdEncoding.EncodeToString([]byte(f.SecretKey)) + code, err := totp.GenerateCode(secret, time.Now().UTC()) + require.NoError(ts.T(), err) + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "challenge_id": c.ID, + "code": code, + })) + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + ts.Require().NoError(err) + require.NoError(ts.T(), user.EnableMFA(ts.API.db)) + + token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + + // data := make(map[string]interface{}) + // require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) +} From 327c9f1433e0438c8683ec6e203eb93c2e4572d8 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 11 Jul 2022 19:55:18 +0800 Subject: [PATCH 059/180] refactor: add struct tags --- api/api.go | 2 +- api/mfa.go | 14 +++++++------- api/mfa_test.go | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/api.go b/api/api.go index 99fafcd99..5c11c154a 100644 --- a/api/api.go +++ b/api/api.go @@ -189,7 +189,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Put("/disable", api.DisableMFA) r.Put("/enable", api.EnableMFA) r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) - r.Post("/enroll_factor", api.EnrollFactor) + r.Post("/factor", api.EnrollFactor) }) }) }) diff --git a/api/mfa.go b/api/mfa.go index 469ed6087..f5f10249a 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -20,15 +20,15 @@ type EnrollFactorParams struct { } type TOTPObject struct { - QRCode string - Secret string - URI string + QRCode string `json:"qr_code"` + Secret string `json:"secret"` + URI string `json:"uri"` } type EnrollFactorResponse struct { - ID string - CreatedAt string - Type string + ID string `json:"id"` + CreatedAt string `json:"created_at"` + Type string `json:"type"` TOTP TOTPObject } @@ -139,7 +139,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { jsonDecoder := json.NewDecoder(r.Body) err := jsonDecoder.Decode(params) if err != nil { - return badRequestError("Could not read EnrollFactor params: %v", err) + return badRequestError(err.Error()) } if (params.FactorType != "totp") && (params.FactorType != "webauthn") { return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") diff --git a/api/mfa_test.go b/api/mfa_test.go index 0f6cf5ae2..f334363ac 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -146,7 +146,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/enroll_factor", user.ID), &buffer) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/factor", user.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") ts.API.handler.ServeHTTP(w, req) From f176f92b55a06bf8e00dc36666762bc8acc0c70b Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 12 Jul 2022 16:11:56 +0800 Subject: [PATCH 060/180] refactor: add struct tas and change route names --- api/api.go | 4 ++-- api/mfa.go | 14 +++++++------- api/mfa_test.go | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/api/api.go b/api/api.go index f04fd7fac..f33c63c23 100644 --- a/api/api.go +++ b/api/api.go @@ -188,9 +188,9 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadUser) r.Put("/disable", api.DisableMFA) r.Put("/enable", api.EnableMFA) - r.Get("/generate_recovery_codes", api.GenerateRecoveryCodes) + r.Get("/recovery_codes", api.GenerateRecoveryCodes) r.Post("/enroll_factor", api.EnrollFactor) - r.Post("/challenge_factor", api.ChallengeFactor) + r.Post("/challenge", api.ChallengeFactor) }) }) }) diff --git a/api/mfa.go b/api/mfa.go index 4a4703109..2f653eac7 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -39,12 +39,12 @@ type ChallengeFactorParams struct { } type ChallengeFactorResponse struct { - ID string - CreatedAt string - UpdatedAt string - ExpiresAt string - FactorID string - FriendlyName string + ID string `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ExpiresAt string `json:"expires_at"` + FactorID string `json:"factor_id"` + FriendlyName string `json:"friendly_name"` } type RecoveryCodesResponse struct { @@ -104,7 +104,7 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro user := getUser(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { - forbiddenError(MFANotEnabledMsg) + return forbiddenError(MFANotEnabledMsg) } recoveryCodeModels := []*models.RecoveryCode{} var terr error diff --git a/api/mfa_test.go b/api/mfa_test.go index d5cc7e314..335337c6c 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -93,7 +93,7 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/mfa/%s/generate_recovery_codes", user.ID), nil) + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/mfa/%s/recovery_codes", user.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) @@ -228,7 +228,7 @@ func (ts *MFATestSuite) TestChallengeFactor() { "friendly_name": c.friendlyName, })) - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/mfa/%s/challenge_factor", u.ID), &buffer) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/mfa/%s/challenge", u.ID), &buffer) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) From e50710338158826ec50b912c4f3691307a5a6318 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 19 Jul 2022 17:59:45 +0800 Subject: [PATCH 061/180] chore: reintroduce uncommented lines --- api/mfa_test.go | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index 2931e2f28..06161157f 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -2,7 +2,6 @@ package api import ( "bytes" - "encoding/base32" "encoding/json" "fmt" "net/http" @@ -248,16 +247,21 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID2", "totp", models.FactorDisabledState, "secretkey") + + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: "Example.com", + AccountName: "alice@example.com", + }) + + f, err := models.NewFactor(u, "testSimpleName", "testFactorID2", "totp", models.FactorDisabledState, key.Secret()) require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") c, err := models.NewChallenge(f) require.NoError(ts.T(), err, "Error creating test Challenge model") require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") - //TOTP library takes in base32 string - secret := base32.StdEncoding.EncodeToString([]byte(f.SecretKey)) - code, err := totp.GenerateCode(secret, time.Now().UTC()) + // TOTP library takes in base32 string + code, err := totp.GenerateCode(key.Secret(), time.Now().UTC()) require.NoError(ts.T(), err) var buffer bytes.Buffer @@ -265,20 +269,20 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { "challenge_id": c.ID, "code": code, })) - // user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - // ts.Require().NoError(err) - // require.NoError(ts.T(), user.EnableMFA(ts.API.db)) + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + ts.Require().NoError(err) + require.NoError(ts.T(), user.EnableMFA(ts.API.db)) - // token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - // require.NoError(ts.T(), err) + token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) - // w := httptest.NewRecorder() - // req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) - // req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - // ts.API.handler.ServeHTTP(w, req) + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) // TODO(Joel) -- Must fix this -- figure out how to fix the totp code value generated so that we can test this - // require.Equal(ts.T(), http.StatusOK, w.Code) + require.Equal(ts.T(), http.StatusOK, w.Code) - // data := make(map[string]interface{}) - // require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + data := make(map[string]interface{}) + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) } From 46e65863ef14dffaaa7135f3a96115558682798e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 20 Jul 2022 12:18:12 +0800 Subject: [PATCH 062/180] refactor: clean up tests --- api/mfa.go | 8 +++----- api/mfa_test.go | 40 +++++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 6a90004f4..b88d4baf0 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -295,7 +295,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { jsonDecoder := json.NewDecoder(r.Body) err = jsonDecoder.Decode(params) if err != nil { - return badRequestError("Could not read VerifyFactor params: %v", err) + return badRequestError("Please check the params passed into VerifyFactor: %v", err) } factor, err := models.FindFactorByChallengeID(a.db, params.ChallengeID) if err != nil { @@ -323,13 +323,11 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { if err = challenge.Verify(a.db); err != nil { return err } - // TODO: Joel -- substitute this with status constants once main branch is merged in - if factor.Status != "verified" { - if err = factor.UpdateStatus(a.db, "verified"); err != nil { + if factor.Status != models.FactorVerifiedState { + if err = factor.UpdateStatus(a.db, models.FactorVerifiedState); err != nil { return err } } - return nil }) valid := totp.Validate(params.Code, factor.SecretKey) diff --git a/api/mfa_test.go b/api/mfa_test.go index 06161157f..805a19936 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -160,7 +161,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { factors, err := models.FindFactorsByUser(ts.API.db, user) ts.Require().NoError(err) latestFactor := factors[len(factors)-1] - require.Equal(ts.T(), "disabled", latestFactor.Status) + require.Equal(ts.T(), models.FactorDisabledState, latestFactor.Status) if c.FriendlyName != "" { require.Equal(ts.T(), c.FriendlyName, latestFactor.FriendlyName) } @@ -243,35 +244,41 @@ func (ts *MFATestSuite) TestChallengeFactor() { } func (ts *MFATestSuite) TestMFAVerifyFactor() { - u, err := models.NewUser(ts.instanceID, "1234567891", "test123@example.com", "password", ts.Config.JWT.Aud, nil) + testEmail := "test123@Example.com" + testDomain := strings.Split(testEmail, "@")[1] + testFactorType := "totp" + + // Create a User with MFA enabled + u, err := models.NewUser(ts.instanceID, "1234567891", testEmail, "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") + require.NoError(ts.T(), u.EnableMFA(ts.API.db)) - + // Enroll a factor key, err := totp.Generate(totp.GenerateOpts{ - Issuer: "Example.com", - AccountName: "alice@example.com", + Issuer: testDomain, + AccountName: testEmail, }) - - f, err := models.NewFactor(u, "testSimpleName", "testFactorID2", "totp", models.FactorDisabledState, key.Secret()) + sharedSecret := key.Secret() + f, err := models.NewFactor(u, "testSimpleName", "testFactorID2", testFactorType, models.FactorDisabledState, sharedSecret) require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + // Make a challenge c, err := models.NewChallenge(f) require.NoError(ts.T(), err, "Error creating test Challenge model") require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") - // TOTP library takes in base32 string - code, err := totp.GenerateCode(key.Secret(), time.Now().UTC()) - require.NoError(ts.T(), err) + // Verify the user + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, testEmail, ts.Config.JWT.Aud) + ts.Require().NoError(err) + code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) + require.NoError(ts.T(), err) var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ "challenge_id": c.ID, "code": code, })) - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - ts.Require().NoError(err) - require.NoError(ts.T(), user.EnableMFA(ts.API.db)) token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) @@ -280,9 +287,12 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) - // TODO(Joel) -- Must fix this -- figure out how to fix the totp code value generated so that we can test this require.Equal(ts.T(), http.StatusOK, w.Code) - data := make(map[string]interface{}) + // Check response + data := VerifyFactorResponse{} require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.Equal(ts.T(), data.ChallengeID, c.ID) + require.Equal(ts.T(), data.MFAType, testFactorType) + require.Equal(ts.T(), data.Success, "true") } From 44c3baacc267372b057e921958b385c837f909e7 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 20 Jul 2022 13:08:09 +0800 Subject: [PATCH 063/180] chore: add misc config vars and errors --- api/errors.go | 4 ++++ api/mfa.go | 11 ++++++++--- conf/configuration.go | 14 +++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/api/errors.go b/api/errors.go index 72653f964..a8054a713 100644 --- a/api/errors.go +++ b/api/errors.go @@ -123,6 +123,10 @@ func tooManyRequestsError(fmtString string, args ...interface{}) *HTTPError { return httpError(http.StatusTooManyRequests, fmtString, args...) } +func expiredChallengeError(fmtString string, args ...interface{}) *HTTPError { + return httpError(http.StatusUnauthorized, fmtString, args...) +} + // HTTPError is an error with a message and an HTTP status code. type HTTPError struct { Code int `json:"code"` diff --git a/api/mfa.go b/api/mfa.go index b88d4baf0..2c9218f2d 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -213,8 +213,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { }) } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { - const challengeExpiryDuration = 300 ctx := r.Context() + config := a.getConfig(ctx) user := getUser(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { @@ -277,7 +277,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, CreatedAt: creationTime.String(), - ExpiresAt: creationTime.Add(time.Second * challengeExpiryDuration).String(), + ExpiresAt: creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)).String(), FactorID: factor.ID, FriendlyName: factor.FriendlyName, }) @@ -286,6 +286,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { var err error ctx := r.Context() + config := a.getConfig(ctx) user := getUser(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { @@ -312,6 +313,10 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error finding Challenge").WithInternalError(err) } + hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration))) + if hasExpired { + return expiredChallengeError("%v has expired, please verify against another challenge or create a new challenge.", challenge.ID) + } err = a.db.Transaction(func(tx *storage.Connection) error { if err = models.NewAuditLogEntry(tx, instanceID, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ @@ -331,7 +336,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return nil }) valid := totp.Validate(params.Code, factor.SecretKey) - if valid != true { + if !valid { return unauthorizedError("Invalid code entered") } diff --git a/conf/configuration.go b/conf/configuration.go index a9e28ee4d..a58cd5baa 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -13,6 +13,7 @@ import ( ) const defaultMinPasswordLength int = 6 +const defaultChallengeExpiryDuration float64 = 300 // OAuthProviderConfiguration holds all config related to external account providers. type OAuthProviderConfiguration struct { @@ -57,6 +58,12 @@ type JWTConfiguration struct { DefaultGroupName string `json:"default_group_name" split_words:"true"` } +// MFAConfiguration holds all the MFA related Configuration +type MFAConfiguration struct { + Enabled bool `json:"mfa_enabled" default:"false"` + ChallengeExpiryDuration float64 `json:"challenge_expiry_duration" default:"300"` +} + // GlobalConfiguration holds all the configuration that applies to all instances. type GlobalConfiguration struct { API struct { @@ -134,7 +141,8 @@ type MailerConfiguration struct { } type PhoneProviderConfiguration struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled" default:"false"` + ChallengeExpiryDuration int `json:"challenge_expiry_duration" default:"300"` } type SmsProviderConfiguration struct { @@ -199,6 +207,7 @@ type Configuration struct { DisableSignup bool `json:"disable_signup" split_words:"true"` Webhook WebhookConfig `json:"webhook" split_words:"true"` Security SecurityConfiguration `json:"security"` + MFA MFAConfiguration `json:"MFA"` Cookie struct { Key string `json:"key"` Domain string `json:"domain"` @@ -354,6 +363,9 @@ func (config *Configuration) ApplyDefaults() { if config.PasswordMinLength < defaultMinPasswordLength { config.PasswordMinLength = defaultMinPasswordLength } + if config.MFA.ChallengeExpiryDuration < defaultChallengeExpiryDuration { + config.MFA.ChallengeExpiryDuration = defaultChallengeExpiryDuration + } } func (config *Configuration) Value() (driver.Value, error) { From 4504906fb4dfe574f778c0827a885411b8b79c40 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 20 Jul 2022 17:07:33 +0800 Subject: [PATCH 064/180] tests: Add additional case for expried challenge + invalid code --- api/mfa.go | 2 +- api/mfa_test.go | 137 +++++++++++++++++++++++++++++++----------------- 2 files changed, 90 insertions(+), 49 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 2c9218f2d..106d0a2e4 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -58,7 +58,7 @@ type VerifyFactorResponse struct { Success string `json:"success"` } -// RecoveryCodesResponse repreesnts a successful recovery code generation response +// RecoveryCodesResponse represents a successful recovery code generation response type RecoveryCodesResponse struct { RecoveryCodes []string `json:"recovery_codes"` } diff --git a/api/mfa_test.go b/api/mfa_test.go index 805a19936..5e4c6eeab 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -244,55 +244,96 @@ func (ts *MFATestSuite) TestChallengeFactor() { } func (ts *MFATestSuite) TestMFAVerifyFactor() { - testEmail := "test123@Example.com" - testDomain := strings.Split(testEmail, "@")[1] - testFactorType := "totp" - - // Create a User with MFA enabled - u, err := models.NewUser(ts.instanceID, "1234567891", testEmail, "password", ts.Config.JWT.Aud, nil) - require.NoError(ts.T(), err, "Error creating test user model") - require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - require.NoError(ts.T(), u.EnableMFA(ts.API.db)) - - // Enroll a factor - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: testDomain, - AccountName: testEmail, - }) - sharedSecret := key.Secret() - f, err := models.NewFactor(u, "testSimpleName", "testFactorID2", testFactorType, models.FactorDisabledState, sharedSecret) - require.NoError(ts.T(), err, "Error creating test factor model") - require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") - - // Make a challenge - c, err := models.NewChallenge(f) - require.NoError(ts.T(), err, "Error creating test Challenge model") - require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") - - // Verify the user - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, testEmail, ts.Config.JWT.Aud) - ts.Require().NoError(err) - code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) - require.NoError(ts.T(), err) - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "challenge_id": c.ID, - "code": code, - })) + // TODO(Joel): test for cases where challenge has expired and code is invalid + cases := []struct { + desc string + validChallenge bool + validCode bool + expectedHTTPCode int + }{ + { + "Invalid: Valid code and expired challenge", + false, + true, + http.StatusUnauthorized, + }, + { + "Invalid: Invalid code and valid challenge ", + true, + false, + http.StatusUnauthorized, + }, + { + "Valid /verify request", + true, + true, + http.StatusOK, + }, + } + for _, v := range cases { + ts.Run(v.desc, func() { + // Create a User with MFA enabled + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), u.EnableMFA(ts.API.db)) + emailValue, err := u.Email.Value() + require.NoError(ts.T(), err) + testEmail := emailValue.(string) + testDomain := strings.Split(testEmail, "@")[1] + testFactorType := "totp" + // set factor secret + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: testDomain, + AccountName: testEmail, + }) + sharedSecret := key.Secret() + factors, err := models.FindFactorsByUser(ts.API.db, u) + f := factors[0] + f.SecretKey = sharedSecret + require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") + + // Make a challenge + c, err := models.NewChallenge(f) + require.NoError(ts.T(), err, "Error creating test Challenge model") + require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") + if !v.validChallenge { + // Set challenge creation so that it has expired in present time. + c.CreatedAt = time.Now().UTC().Add(-1 * time.Second * time.Duration(ts.Config.MFA.ChallengeExpiryDuration+1)) + require.NoError(ts.T(), ts.API.db.Update(c), "Error updating new test challenge") + } - token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err) + // Verify the user + user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, testEmail, ts.Config.JWT.Aud) + ts.Require().NoError(err) + code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) + if !v.validCode { + // Use an inaccurate time, resulting in an invalid code(usually) + code, err = totp.GenerateCode(sharedSecret, time.Now().UTC().Add(-1*time.Minute*time.Duration(1))) + } + require.NoError(ts.T(), err) + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "challenge_id": c.ID, + "code": code, + })) - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) + token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) - // Check response - data := VerifyFactorResponse{} - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.Equal(ts.T(), data.ChallengeID, c.ID) - require.Equal(ts.T(), data.MFAType, testFactorType) - require.Equal(ts.T(), data.Success, "true") + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), v.expectedHTTPCode, w.Code) + + // Check response + data := VerifyFactorResponse{} + if v.expectedHTTPCode == http.StatusOK { + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.Equal(ts.T(), data.ChallengeID, c.ID) + require.Equal(ts.T(), data.MFAType, testFactorType) + require.Equal(ts.T(), data.Success, "true") + } + }) + } } From 7c06b18eca7f2c3e707fb4161b43d86939b701f4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 21 Jul 2022 15:24:15 +0800 Subject: [PATCH 065/180] fix: patch test --- api/mfa_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index 5e4c6eeab..0b4ff2e9b 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -298,8 +298,10 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") if !v.validChallenge { // Set challenge creation so that it has expired in present time. - c.CreatedAt = time.Now().UTC().Add(-1 * time.Second * time.Duration(ts.Config.MFA.ChallengeExpiryDuration+1)) - require.NoError(ts.T(), ts.API.db.Update(c), "Error updating new test challenge") + newCreatedAt := time.Now().UTC().Add(-1 * time.Second * time.Duration(ts.Config.MFA.ChallengeExpiryDuration+1)) + // created_at is managed by buffalo(ORM) needs to be raw query toe be updated + err := ts.API.db.RawQuery("UPDATE auth.mfa_challenges SET created_at = ? WHERE factor_id = ?", newCreatedAt, f.ID).Exec() + require.NoError(ts.T(), err, "Error updating new test challenge") } // Verify the user From 718b50162c69e496c1608bffb0887db5fde2b333 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 21 Jul 2022 15:31:31 +0800 Subject: [PATCH 066/180] chore: add newlines --- api/mfa.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/mfa.go b/api/mfa.go index 106d0a2e4..23e4a98e3 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -292,12 +292,14 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { if !user.MFAEnabled { return forbiddenError(MFANotEnabledMsg) } + params := &VerifyFactorParams{} jsonDecoder := json.NewDecoder(r.Body) err = jsonDecoder.Decode(params) if err != nil { return badRequestError("Please check the params passed into VerifyFactor: %v", err) } + factor, err := models.FindFactorByChallengeID(a.db, params.ChallengeID) if err != nil { if models.IsNotFoundError(err) { @@ -305,6 +307,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return internalServerError("Database error finding factor").WithInternalError(err) } + challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) if err != nil { if models.IsNotFoundError(err) { @@ -337,7 +340,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { }) valid := totp.Validate(params.Code, factor.SecretKey) if !valid { - return unauthorizedError("Invalid code entered") + return unauthorizedError("Invalid TOTP code entered") } return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ From 1c71c1d567e795e55a46081cba684f8427e736f4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 21 Jul 2022 15:43:04 +0800 Subject: [PATCH 067/180] chore: remove stray comment --- api/mfa_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index 0b4ff2e9b..a30c862d4 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -244,7 +244,6 @@ func (ts *MFATestSuite) TestChallengeFactor() { } func (ts *MFATestSuite) TestMFAVerifyFactor() { - // TODO(Joel): test for cases where challenge has expired and code is invalid cases := []struct { desc string validChallenge bool From d70fce43126b237f95e88c8ffd279cf71f6124d9 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 10:47:05 +0800 Subject: [PATCH 068/180] refactor: delete challenge if expired --- api/mfa.go | 13 ++++++++++++- api/mfa_test.go | 4 ++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/api/mfa.go b/api/mfa.go index 23e4a98e3..ed52c48bf 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -314,10 +314,21 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return notFoundError(err.Error()) } return internalServerError("Database error finding Challenge").WithInternalError(err) - } + hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration))) if hasExpired { + err := a.db.Transaction(func(tx *storage.Connection) error { + if terr := tx.Destroy(challenge); terr != nil { + return internalServerError("Database error deleting challenge").WithInternalError(terr) + } + + return nil + }) + if err != nil { + return err + } + return expiredChallengeError("%v has expired, please verify against another challenge or create a new challenge.", challenge.ID) } diff --git a/api/mfa_test.go b/api/mfa_test.go index a30c862d4..b7c8cf6c1 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -335,6 +335,10 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { require.Equal(ts.T(), data.MFAType, testFactorType) require.Equal(ts.T(), data.Success, "true") } + if !v.validChallenge { + _, err := models.FindChallengeByChallengeID(ts.API.db, c.ID) + require.EqualError(ts.T(), err, models.ChallengeNotFoundError{}.Error()) + } }) } } From beddffd1ac580b3bad0a5cfad53e1a04e469101e Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 13:20:44 +0800 Subject: [PATCH 069/180] feat: remove unused fields, change recovery_codes from GET to POST --- api/api.go | 2 +- api/mfa.go | 38 ++++++++++++-------------------------- api/mfa_test.go | 28 ++++------------------------ 3 files changed, 17 insertions(+), 51 deletions(-) diff --git a/api/api.go b/api/api.go index f3d2b6098..3e88aafc6 100644 --- a/api/api.go +++ b/api/api.go @@ -188,7 +188,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadUser) r.Put("/disable", api.DisableMFA) r.Put("/enable", api.EnableMFA) - r.Get("/recovery_codes", api.GenerateRecoveryCodes) + r.Post("/recovery_codes", api.GenerateRecoveryCodes) r.Post("/factor", api.EnrollFactor) r.Post("/challenge", api.ChallengeFactor) r.Post("/verify", api.VerifyFactor) diff --git a/api/mfa.go b/api/mfa.go index ed52c48bf..447e6ffad 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -44,18 +44,15 @@ type VerifyFactorParams struct { } type ChallengeFactorResponse struct { - ID string `json:"id"` - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` - ExpiresAt string `json:"expires_at"` - FactorID string `json:"factor_id"` - FriendlyName string `json:"friendly_name"` + ID string `json:"id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + ExpiresAt string `json:"expires_at"` } type VerifyFactorResponse struct { - ChallengeID string `json:"challenge_id"` - MFAType string `json:"mfa_type"` - Success string `json:"success"` + MFAType string `json:"mfa_type"` + Success string `json:"success"` } // RecoveryCodesResponse represents a successful recovery code generation response @@ -230,18 +227,11 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError("Could not read EnrollFactor params: %v", err) } factorID := params.FactorID - friendlyName := params.FriendlyName - - if factorID != "" && friendlyName != "" { - return unprocessableEntityError("Only a FactorID or FactorSimpleName should be provided on signup.") - } if factorID != "" { factor, err = models.FindFactorByFactorID(a.db, factorID) - } else if friendlyName != "" { - factor, err = models.FindFactorByFriendlyName(a.db, friendlyName) } else { - return unprocessableEntityError("Either FactorID or FactorSimpleName should be provided on signup.") + return unprocessableEntityError("FactorID should be provided to create a challenge") } if err != nil { if models.IsNotFoundError(err) { @@ -261,7 +251,6 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ "factor_id": params.FactorID, - "friendly_name": params.FriendlyName, "factor_status": factor.Status, }); terr != nil { return terr @@ -275,11 +264,9 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ - ID: challenge.ID, - CreatedAt: creationTime.String(), - ExpiresAt: creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)).String(), - FactorID: factor.ID, - FriendlyName: factor.FriendlyName, + ID: challenge.ID, + CreatedAt: creationTime.String(), + ExpiresAt: creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)).String(), }) } @@ -355,9 +342,8 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ - ChallengeID: params.ChallengeID, - MFAType: factor.FactorType, - Success: fmt.Sprintf("%v", valid), + MFAType: factor.FactorType, + Success: fmt.Sprintf("%v", valid), }) } diff --git a/api/mfa_test.go b/api/mfa_test.go index b7c8cf6c1..c5fa5152e 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -94,7 +94,7 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/mfa/%s/recovery_codes", user.ID), nil) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/recovery_codes", user.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) @@ -174,41 +174,23 @@ func (ts *MFATestSuite) TestChallengeFactor() { cases := []struct { desc string id string - friendlyName string mfaEnabled bool expectedCode int }{ { "MFA Not Enabled", "", - "", false, http.StatusForbidden, }, { - "Both Factor ID and Simple Name are present", + "Factor ID present", "testFactorID", - "testSimpleFactor", - true, - http.StatusUnprocessableEntity, - }, - { - "Only factor simple name", - "", - "testSimpleName", true, http.StatusOK, }, { - "Only factor ID", - "testFactorID", - "", - true, - http.StatusOK, - }, - { - "Both factor and simple name missing", - "", + "Factor ID missing", "", true, http.StatusUnprocessableEntity, @@ -228,8 +210,7 @@ func (ts *MFATestSuite) TestChallengeFactor() { var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "factor_id": c.id, - "friendly_name": c.friendlyName, + "factor_id": c.id, })) req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/mfa/%s/challenge", u.ID), &buffer) @@ -331,7 +312,6 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { data := VerifyFactorResponse{} if v.expectedHTTPCode == http.StatusOK { require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.Equal(ts.T(), data.ChallengeID, c.ID) require.Equal(ts.T(), data.MFAType, testFactorType) require.Equal(ts.T(), data.Success, "true") } From a3ed02172a0e7ad8be1aecf62247b9e213a550ca Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 13:31:01 +0800 Subject: [PATCH 070/180] refactor: remove type field from endpoint --- api/mfa.go | 2 -- api/mfa_test.go | 2 -- 2 files changed, 4 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 447e6ffad..7cc55550b 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -51,7 +51,6 @@ type ChallengeFactorResponse struct { } type VerifyFactorResponse struct { - MFAType string `json:"mfa_type"` Success string `json:"success"` } @@ -342,7 +341,6 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ - MFAType: factor.FactorType, Success: fmt.Sprintf("%v", valid), }) diff --git a/api/mfa_test.go b/api/mfa_test.go index c5fa5152e..ba4f86c2f 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -259,7 +259,6 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { require.NoError(ts.T(), err) testEmail := emailValue.(string) testDomain := strings.Split(testEmail, "@")[1] - testFactorType := "totp" // set factor secret key, err := totp.Generate(totp.GenerateOpts{ Issuer: testDomain, @@ -312,7 +311,6 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { data := VerifyFactorResponse{} if v.expectedHTTPCode == http.StatusOK { require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.Equal(ts.T(), data.MFAType, testFactorType) require.Equal(ts.T(), data.Success, "true") } if !v.validChallenge { From 237f69a2b8771ff39de087d9157d98d718474048 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 26 Jul 2022 04:30:02 +0000 Subject: [PATCH 071/180] chore: speed up tests (#564) On a not-so-good laptop, a full test run went from ~90seconds to less than 10. Three independent changes were made: 1 - In TruncateAll, use `delete from` instead of `truncate`. While truncate is faster for large tables, for small tables, it has considerably more overhead (my understanding is that delete just flags the tuple as dead, whereas truncate involves a vacuum-like operation). 2 - In tests, use bcrypt.MinCost instead of bcrypt.DefaultCost 3 - Use a 10 millisecond timeout, instead of 1 second timeout, for TestHookTimeout --- api/api_test.go | 3 +++ api/hook_test.go | 13 +++++++++---- hack/test.env | 3 ++- models/connection.go | 8 ++++---- models/user.go | 3 ++- models/user_test.go | 5 +++++ 6 files changed, 25 insertions(+), 10 deletions(-) diff --git a/api/api_test.go b/api/api_test.go index a5522f60e..bfa78fec4 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -12,6 +12,7 @@ import ( "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" "github.com/stretchr/testify/require" + "golang.org/x/crypto/bcrypt" ) const ( @@ -21,6 +22,8 @@ const ( func init() { rand.Seed(time.Now().UnixNano()) + models.PasswordHashCost = bcrypt.MinCost + } // setupAPIForTest creates a new API to run tests with. diff --git a/api/hook_test.go b/api/hook_test.go index 26a6e7ce1..16d0105ca 100644 --- a/api/hook_test.go +++ b/api/hook_test.go @@ -142,10 +142,16 @@ func TestHookRetry(t *testing.T) { } func TestHookTimeout(t *testing.T) { + realTimeout := defaultTimeout + defer func() { + defaultTimeout = realTimeout + }() + defaultTimeout = time.Millisecond * 10 + var callCount int svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ - <-time.After(2 * time.Second) + time.Sleep(20 * time.Millisecond) })) defer svr.Close() @@ -154,9 +160,8 @@ func TestHookTimeout(t *testing.T) { defer unshiftPrivateIPBlock(localhost) config := &conf.WebhookConfig{ - URL: svr.URL, - Retries: 3, - TimeoutSec: 1, + URL: svr.URL, + Retries: 3, } w := Webhook{ WebhookConfig: config, diff --git a/hack/test.env b/hack/test.env index b1dcd884a..ceebc2add 100644 --- a/hack/test.env +++ b/hack/test.env @@ -86,7 +86,8 @@ GOTRUE_EXTERNAL_SAML_ENABLED=true GOTRUE_EXTERNAL_SAML_METADATA_URL= GOTRUE_EXTERNAL_SAML_API_BASE=http://localhost GOTRUE_EXTERNAL_SAML_NAME=TestSamlName -GOTRUE_RATE_LIMIT_VERIFY="1000" +GOTRUE_RATE_LIMIT_VERIFY="100000" +GOTRUE_RATE_LIMIT_TOKEN_REFRESH="100000" GOTRUE_TRACING_ENABLED=false GOTRUE_TRACING_HOST=127.0.0.1 GOTRUE_TRACING_PORT=8126 diff --git a/models/connection.go b/models/connection.go index 7c01a1de8..94696a796 100644 --- a/models/connection.go +++ b/models/connection.go @@ -32,15 +32,15 @@ type SortField struct { func TruncateAll(conn *storage.Connection) error { return conn.Transaction(func(tx *storage.Connection) error { - if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: User{}}).TableName() + " CASCADE").Exec(); err != nil { + if err := tx.RawQuery("delete from " + (&pop.Model{Value: User{}}).TableName()).Exec(); err != nil { return err } - if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: RefreshToken{}}).TableName() + " CASCADE").Exec(); err != nil { + if err := tx.RawQuery("delete from " + (&pop.Model{Value: RefreshToken{}}).TableName()).Exec(); err != nil { return err } - if err := tx.RawQuery("TRUNCATE " + (&pop.Model{Value: AuditLogEntry{}}).TableName() + " CASCADE").Exec(); err != nil { + if err := tx.RawQuery("delete from " + (&pop.Model{Value: AuditLogEntry{}}).TableName()).Exec(); err != nil { return err } - return tx.RawQuery("TRUNCATE " + (&pop.Model{Value: Instance{}}).TableName() + " CASCADE").Exec() + return tx.RawQuery("delete from " + (&pop.Model{Value: Instance{}}).TableName()).Exec() }) } diff --git a/models/user.go b/models/user.go index 46bba3c4f..52003e532 100644 --- a/models/user.go +++ b/models/user.go @@ -16,6 +16,7 @@ import ( const SystemUserID = "0" var SystemUserUUID = uuid.Nil +var PasswordHashCost = bcrypt.DefaultCost // User respresents a registered user with email/password authentication type User struct { @@ -253,7 +254,7 @@ func (u *User) SetPhone(tx *storage.Connection, phone string) error { // hashPassword generates a hashed password from a plaintext string func hashPassword(password string) (string, error) { - pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + pw, err := bcrypt.GenerateFromPassword([]byte(password), PasswordHashCost) if err != nil { return "", err } diff --git a/models/user_test.go b/models/user_test.go index 28771df4f..56a2803f7 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -10,10 +10,15 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" ) const modelsTestConfig = "../hack/test.env" +func init() { + PasswordHashCost = bcrypt.MinCost +} + type UserTestSuite struct { suite.Suite db *storage.Connection From c65c136f5507653d81801e98f28f60a9f69077b4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 14:53:44 +0800 Subject: [PATCH 072/180] feat: add admin delete methods --- api/admin.go | 68 +++++++++++++++++++++++++++++++++++++++ api/admin_test.go | 59 +++++++++++++++++++++++++++++++++ api/api.go | 5 +++ api/mfa.go | 6 ++-- models/audit_log_entry.go | 6 ++++ models/recovery_code.go | 3 ++ 6 files changed, 143 insertions(+), 4 deletions(-) diff --git a/api/admin.go b/api/admin.go index 2134ce2d3..e277a547e 100644 --- a/api/admin.go +++ b/api/admin.go @@ -28,6 +28,10 @@ type adminUserParams struct { BanDuration string `json:"ban_duration"` } +type AdminUserDeleteFactorParams struct { + FactorID string `json:"factor_id"` +} + func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, error) { userID, err := uuid.FromString(chi.URLParam(r, "user_id")) if err != nil { @@ -342,3 +346,67 @@ func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, map[string]interface{}{}) } + +func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + + params := &AdminUserDeleteFactorParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Invalid parameters: Please re-check request parameters: %v", err) + } + + factor, terr := models.FindFactorByFactorID(a.db, params.FactorID) + if terr != nil { + return terr + } + err = a.db.Transaction(func(tx *storage.Connection) error { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.FactorModifiedAction, r.RemoteAddr, map[string]interface{}{ + "user_id": user.ID, + "factor_id": factor.ID, + }); terr != nil { + return terr + } + if terr := tx.Destroy(factor); terr != nil { + return internalServerError("Database error deleting factor").WithInternalError(terr) + } + return nil + }) + if err != nil { + return err + } + return sendJSON(w, http.StatusOK, factor) + +} + +func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + + recoveryCodes, terr := models.FindValidRecoveryCodesByUser(a.db, user) + if terr != nil { + return terr + } + terr = a.db.Transaction(func(tx *storage.Connection) error { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteRecoveryCodesAction, r.RemoteAddr, map[string]interface{}{ + "user_id": user.ID, + }); terr != nil { + return terr + } + for _, recoveryCodeModel := range recoveryCodes { + if terr := tx.Destroy(recoveryCodeModel); terr != nil { + return terr + } + } + return nil + }) + if terr != nil { + return terr + } + + return sendJSON(w, http.StatusOK, map[string]interface{}{}) +} diff --git a/api/admin_test.go b/api/admin_test.go index b6e450c69..269f207e5 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -12,6 +12,7 @@ import ( "github.com/gofrs/uuid" jwt "github.com/golang-jwt/jwt" "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -597,3 +598,61 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { }) } } + +// TestAdminUserDelete tests API /admin/users//mfa/factor route (DELETE) +func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { + u, err := models.NewUser(ts.instanceID, "123456789", "test-factor-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + f, err := models.NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "") + require.NoError(ts.T(), err, "Error making new factor") + require.NoError(ts.T(), ts.API.db.Create(f), "Error creating factor") + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "factor_id": f.ID, + })) + // Setup request + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/mfa/factor", u.ID), &buffer) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + _, err = models.FindFactorByFactorID(ts.API.db, f.ID) + require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) +} + +// TestAdminUserDelete tests API /admin/users//mfa/recovery_codes route (DELETE) +func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { + u, err := models.NewUser(ts.instanceID, "123456789", "test-factor-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + f, err := models.NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "") + require.NoError(ts.T(), err, "Error making new factor") + require.NoError(ts.T(), ts.API.db.Create(f), "Error creating factor") + + for i := 0; i <= models.NumRecoveryCodes; i++ { + rc, err := models.NewRecoveryCode(u, crypto.SecureToken(models.RecoveryCodeLength)) + require.NoError(ts.T(), err) + err = ts.API.db.Create(rc) + require.NoError(ts.T(), err) + } + + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{})) + // Setup request + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/mfa/recovery_codes", u.ID), &buffer) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + recoveryCodes, err := models.FindValidRecoveryCodesByUser(ts.API.db, u) + expectedRecoveryCodes := []*models.RecoveryCode{} + require.Equal(ts.T(), expectedRecoveryCodes, recoveryCodes) +} diff --git a/api/api.go b/api/api.go index 3e88aafc6..86e8b5621 100644 --- a/api/api.go +++ b/api/api.go @@ -165,10 +165,15 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) + r.Route("/mfa", func(r *router) { + r.Delete("/factor", api.adminUserDeleteFactor) + r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) + }) r.Get("/", api.adminUserGet) r.Put("/", api.adminUserUpdate) r.Delete("/", api.adminUserDelete) + }) }) diff --git a/api/mfa.go b/api/mfa.go index 7cc55550b..df1fdf377 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -106,8 +106,6 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { } func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { - const numRecoveryCodes = 8 - const recoveryCodeLength = 8 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -119,8 +117,8 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro var recoveryCode string var recoveryCodes []string var recoveryCodeModel *models.RecoveryCode - for i := 0; i < numRecoveryCodes; i++ { - recoveryCode = crypto.SecureToken(recoveryCodeLength) + for i := 0; i < models.NumRecoveryCodes; i++ { + recoveryCode = crypto.SecureToken(models.RecoveryCodeLength) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) if terr != nil { return internalServerError("Error creating recovery code").WithInternalError(terr) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 5d802a360..c9198984a 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -32,6 +32,9 @@ const ( EnrollFactorAction AuditAction = "factor_enrolled" CreateChallengeAction AuditAction = "challenge_created" VerifyFactorAction AuditAction = "verification_attempted" + DeleteFactorAction AuditAction = "factor_deleted" + FactorModifiedAction AuditAction = "factor_modified" + DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" account auditLogType = "account" team auditLogType = "team" @@ -57,6 +60,9 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ EnrollFactorAction: factor, CreateChallengeAction: factor, VerifyFactorAction: factor, + DeleteFactorAction: factor, + FactorModifiedAction: factor, + DeleteRecoveryCodesAction: team, } // AuditLogEntry is the database model for audit log entries. diff --git a/models/recovery_code.go b/models/recovery_code.go index 244935982..133140c3b 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -8,6 +8,9 @@ import ( "time" ) +const NumRecoveryCodes = 8 +const RecoveryCodeLength = 8 + type RecoveryCode struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` From 524c990c327c1f6730c6190b0b3d0c51ec912585 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 15:22:28 +0800 Subject: [PATCH 073/180] feat: add unenroll endpoint --- api/api.go | 1 + api/mfa.go | 55 +++++++++++++++++++++++++++++++++++++++ api/mfa_test.go | 40 ++++++++++++++++++++++++++++ models/audit_log_entry.go | 2 ++ 4 files changed, 98 insertions(+) diff --git a/api/api.go b/api/api.go index 86e8b5621..a3d9e36ba 100644 --- a/api/api.go +++ b/api/api.go @@ -195,6 +195,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Put("/enable", api.EnableMFA) r.Post("/recovery_codes", api.GenerateRecoveryCodes) r.Post("/factor", api.EnrollFactor) + r.Post("/unenroll", api.UnenrollFactor) r.Post("/challenge", api.ChallengeFactor) r.Post("/verify", api.VerifyFactor) diff --git a/api/mfa.go b/api/mfa.go index df1fdf377..716450507 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -54,6 +54,15 @@ type VerifyFactorResponse struct { Success string `json:"success"` } +type UnenrollFactorResponse struct { + Success string `json:"success"` +} + +type UnenrollFactorParams struct { + FactorID string `json:"factor_id"` + Code string `json:"code"` +} + // RecoveryCodesResponse represents a successful recovery code generation response type RecoveryCodesResponse struct { RecoveryCodes []string `json:"recovery_codes"` @@ -343,3 +352,49 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { }) } + +func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { + var err error + ctx := r.Context() + user := getUser(ctx) + instanceID := getInstanceID(ctx) + if !user.MFAEnabled { + return forbiddenError(MFANotEnabledMsg) + } + params := &UnenrollFactorParams{} + jsonDecoder := json.NewDecoder(r.Body) + err = jsonDecoder.Decode(params) + if err != nil { + return badRequestError(err.Error()) + } + + factor, err := models.FindFactorByFactorID(a.db, params.FactorID) + if err != nil { + if models.IsNotFoundError(err) { + return notFoundError(err.Error()) + } + return internalServerError("Database error finding factor").WithInternalError(err) + } + + valid := totp.Validate(params.Code, factor.SecretKey) + if valid != true { + return unauthorizedError("Invalid code entered") + } + + err = a.db.Transaction(func(tx *storage.Connection) error { + if err = factor.UpdateStatus(a.db, models.FactorDisabledState); err != nil { + return err + } + if err = models.NewAuditLogEntry(tx, instanceID, user, models.UnenrollFactorAction, r.RemoteAddr, map[string]interface{}{ + "user_id": user.ID, + "factor_id": factor.ID, + }); err != nil { + return err + } + return nil + }) + + return sendJSON(w, http.StatusOK, &UnenrollFactorResponse{ + Success: fmt.Sprintf("%v", valid), + }) +} diff --git a/api/mfa_test.go b/api/mfa_test.go index ba4f86c2f..3ad127acb 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -320,3 +320,43 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { }) } } + +func (ts *MFATestSuite) TestUnenrollFactor() { + // Create a User with MFA enabled + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), u.EnableMFA(ts.API.db)) + emailValue, err := u.Email.Value() + require.NoError(ts.T(), err) + testEmail := emailValue.(string) + testDomain := strings.Split(testEmail, "@")[1] + // set factor secret + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: testDomain, + AccountName: testEmail, + }) + sharedSecret := key.Secret() + factors, err := models.FindFactorsByUser(ts.API.db, u) + f := factors[0] + f.SecretKey = sharedSecret + require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") + + var buffer bytes.Buffer + + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) + require.NoError(ts.T(), err) + + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "factor_id": f.ID, + "code": code, + })) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/unenroll", u.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) +} diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index c9198984a..cb72ac956 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -30,6 +30,7 @@ const ( TokenRefreshedAction AuditAction = "token_refreshed" GenerateRecoveryCodesAction AuditAction = "generate_recovery_codes" EnrollFactorAction AuditAction = "factor_enrolled" + UnenrollFactorAction AuditAction = "factor_unenrolled" CreateChallengeAction AuditAction = "challenge_created" VerifyFactorAction AuditAction = "verification_attempted" DeleteFactorAction AuditAction = "factor_deleted" @@ -58,6 +59,7 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ UserRepeatedSignUpAction: user, GenerateRecoveryCodesAction: user, EnrollFactorAction: factor, + UnenrollFactorAction: factor, CreateChallengeAction: factor, VerifyFactorAction: factor, DeleteFactorAction: factor, From 0b63c9cbbaebe27f4e6071ff17ddbe71512d512b Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 15:22:37 +0800 Subject: [PATCH 074/180] Revert "feat: add admin delete methods" This reverts commit c65c136f5507653d81801e98f28f60a9f69077b4. --- api/admin.go | 68 --------------------------------------- api/admin_test.go | 59 --------------------------------- api/api.go | 5 --- api/mfa.go | 6 ++-- models/audit_log_entry.go | 6 ---- models/recovery_code.go | 3 -- 6 files changed, 4 insertions(+), 143 deletions(-) diff --git a/api/admin.go b/api/admin.go index e277a547e..2134ce2d3 100644 --- a/api/admin.go +++ b/api/admin.go @@ -28,10 +28,6 @@ type adminUserParams struct { BanDuration string `json:"ban_duration"` } -type AdminUserDeleteFactorParams struct { - FactorID string `json:"factor_id"` -} - func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, error) { userID, err := uuid.FromString(chi.URLParam(r, "user_id")) if err != nil { @@ -346,67 +342,3 @@ func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, map[string]interface{}{}) } - -func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - - params := &AdminUserDeleteFactorParams{} - jsonDecoder := json.NewDecoder(r.Body) - err := jsonDecoder.Decode(params) - if err != nil { - return badRequestError("Invalid parameters: Please re-check request parameters: %v", err) - } - - factor, terr := models.FindFactorByFactorID(a.db, params.FactorID) - if terr != nil { - return terr - } - err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.FactorModifiedAction, r.RemoteAddr, map[string]interface{}{ - "user_id": user.ID, - "factor_id": factor.ID, - }); terr != nil { - return terr - } - if terr := tx.Destroy(factor); terr != nil { - return internalServerError("Database error deleting factor").WithInternalError(terr) - } - return nil - }) - if err != nil { - return err - } - return sendJSON(w, http.StatusOK, factor) - -} - -func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - - recoveryCodes, terr := models.FindValidRecoveryCodesByUser(a.db, user) - if terr != nil { - return terr - } - terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteRecoveryCodesAction, r.RemoteAddr, map[string]interface{}{ - "user_id": user.ID, - }); terr != nil { - return terr - } - for _, recoveryCodeModel := range recoveryCodes { - if terr := tx.Destroy(recoveryCodeModel); terr != nil { - return terr - } - } - return nil - }) - if terr != nil { - return terr - } - - return sendJSON(w, http.StatusOK, map[string]interface{}{}) -} diff --git a/api/admin_test.go b/api/admin_test.go index 269f207e5..b6e450c69 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -12,7 +12,6 @@ import ( "github.com/gofrs/uuid" jwt "github.com/golang-jwt/jwt" "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -598,61 +597,3 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { }) } } - -// TestAdminUserDelete tests API /admin/users//mfa/factor route (DELETE) -func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-factor-delete@example.com", "test", ts.Config.JWT.Aud, nil) - require.NoError(ts.T(), err, "Error making new user") - require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - - f, err := models.NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "") - require.NoError(ts.T(), err, "Error making new factor") - require.NoError(ts.T(), ts.API.db.Create(f), "Error creating factor") - - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "factor_id": f.ID, - })) - // Setup request - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/mfa/factor", u.ID), &buffer) - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) - - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) - _, err = models.FindFactorByFactorID(ts.API.db, f.ID) - require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) -} - -// TestAdminUserDelete tests API /admin/users//mfa/recovery_codes route (DELETE) -func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-factor-delete@example.com", "test", ts.Config.JWT.Aud, nil) - require.NoError(ts.T(), err, "Error making new user") - require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - - f, err := models.NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "") - require.NoError(ts.T(), err, "Error making new factor") - require.NoError(ts.T(), ts.API.db.Create(f), "Error creating factor") - - for i := 0; i <= models.NumRecoveryCodes; i++ { - rc, err := models.NewRecoveryCode(u, crypto.SecureToken(models.RecoveryCodeLength)) - require.NoError(ts.T(), err) - err = ts.API.db.Create(rc) - require.NoError(ts.T(), err) - } - - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{})) - // Setup request - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/mfa/recovery_codes", u.ID), &buffer) - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) - - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) - recoveryCodes, err := models.FindValidRecoveryCodesByUser(ts.API.db, u) - expectedRecoveryCodes := []*models.RecoveryCode{} - require.Equal(ts.T(), expectedRecoveryCodes, recoveryCodes) -} diff --git a/api/api.go b/api/api.go index a3d9e36ba..fc2b22822 100644 --- a/api/api.go +++ b/api/api.go @@ -165,15 +165,10 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) - r.Route("/mfa", func(r *router) { - r.Delete("/factor", api.adminUserDeleteFactor) - r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) - }) r.Get("/", api.adminUserGet) r.Put("/", api.adminUserUpdate) r.Delete("/", api.adminUserDelete) - }) }) diff --git a/api/mfa.go b/api/mfa.go index 716450507..f4a29f565 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -115,6 +115,8 @@ func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { } func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { + const numRecoveryCodes = 8 + const recoveryCodeLength = 8 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -126,8 +128,8 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro var recoveryCode string var recoveryCodes []string var recoveryCodeModel *models.RecoveryCode - for i := 0; i < models.NumRecoveryCodes; i++ { - recoveryCode = crypto.SecureToken(models.RecoveryCodeLength) + for i := 0; i < numRecoveryCodes; i++ { + recoveryCode = crypto.SecureToken(recoveryCodeLength) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) if terr != nil { return internalServerError("Error creating recovery code").WithInternalError(terr) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index cb72ac956..40363a3e9 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -33,9 +33,6 @@ const ( UnenrollFactorAction AuditAction = "factor_unenrolled" CreateChallengeAction AuditAction = "challenge_created" VerifyFactorAction AuditAction = "verification_attempted" - DeleteFactorAction AuditAction = "factor_deleted" - FactorModifiedAction AuditAction = "factor_modified" - DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" account auditLogType = "account" team auditLogType = "team" @@ -62,9 +59,6 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ UnenrollFactorAction: factor, CreateChallengeAction: factor, VerifyFactorAction: factor, - DeleteFactorAction: factor, - FactorModifiedAction: factor, - DeleteRecoveryCodesAction: team, } // AuditLogEntry is the database model for audit log entries. diff --git a/models/recovery_code.go b/models/recovery_code.go index 133140c3b..244935982 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -8,9 +8,6 @@ import ( "time" ) -const NumRecoveryCodes = 8 -const RecoveryCodeLength = 8 - type RecoveryCode struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` From 0c365863893e7da7ebfe950e072949b15234c559 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 26 Jul 2022 15:50:12 +0800 Subject: [PATCH 075/180] fix: change behavior of unenroll --- api/mfa.go | 2 +- api/mfa_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api/mfa.go b/api/mfa.go index f4a29f565..5bd57521a 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -384,7 +384,7 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if err = factor.UpdateStatus(a.db, models.FactorDisabledState); err != nil { + if err = tx.Destroy(factor); err != nil { return err } if err = models.NewAuditLogEntry(tx, instanceID, user, models.UnenrollFactorAction, r.RemoteAddr, map[string]interface{}{ diff --git a/api/mfa_test.go b/api/mfa_test.go index 3ad127acb..3221cfb89 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -359,4 +359,7 @@ func (ts *MFATestSuite) TestUnenrollFactor() { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) + _, err = models.FindFactorByFactorID(ts.API.db, f.ID) + require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) + } From f68aedb60c584c30f912f16d991e26c04dcbabd1 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 27 Jul 2022 14:27:06 +0800 Subject: [PATCH 076/180] refactor: strip out /enable and /disable --- api/api.go | 2 -- api/mfa.go | 46 ---------------------------------------------- api/mfa_test.go | 33 --------------------------------- 3 files changed, 81 deletions(-) diff --git a/api/api.go b/api/api.go index 86e8b5621..8d983a20a 100644 --- a/api/api.go +++ b/api/api.go @@ -191,8 +191,6 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/mfa", func(r *router) { r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) - r.Put("/disable", api.DisableMFA) - r.Put("/enable", api.EnableMFA) r.Post("/recovery_codes", api.GenerateRecoveryCodes) r.Post("/factor", api.EnrollFactor) r.Post("/challenge", api.ChallengeFactor) diff --git a/api/mfa.go b/api/mfa.go index df1fdf377..02e40187d 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -59,52 +59,6 @@ type RecoveryCodesResponse struct { RecoveryCodes []string `json:"recovery_codes"` } -func (a *API) EnableMFA(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - err := a.db.Transaction(func(tx *storage.Connection) error { - if terr := user.EnableMFA(tx); terr != nil { - return terr - } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, r.RemoteAddr, map[string]interface{}{ - "user_id": user.ID, - "user_email": user.Email, - "user_phone": user.Phone, - }); terr != nil { - return terr - } - return nil - }) - if err != nil { - return err - } - return sendJSON(w, http.StatusOK, user) -} - -func (a *API) DisableMFA(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - err := a.db.Transaction(func(tx *storage.Connection) error { - if terr := user.DisableMFA(tx); terr != nil { - return terr - } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.UserModifiedAction, r.RemoteAddr, map[string]interface{}{ - "user_id": user.ID, - "user_email": user.Email, - "user_phone": user.Phone, - }); terr != nil { - return terr - } - return nil - }) - if err != nil { - return err - } - return sendJSON(w, http.StatusOK, user) -} - func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) diff --git a/api/mfa_test.go b/api/mfa_test.go index ba4f86c2f..4b01d7635 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -51,39 +51,6 @@ func (ts *MFATestSuite) SetupTest() { require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") } -func (ts *MFATestSuite) TestMFAEnable() { - u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err) - - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/enable", u.ID), nil) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), w.Code, http.StatusOK) - - u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), err) - require.True(ts.T(), u.MFAEnabled) -} - -func (ts *MFATestSuite) TestMFADisable() { - u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), u.EnableMFA(ts.API.db)) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err) - - req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("http://localhost/mfa/%s/disable", u.ID), nil) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), w.Code, http.StatusOK) - - u, err = models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), err) - require.False(ts.T(), u.MFAEnabled) -} - func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { const expectedNumOfRecoveryCodes = 8 user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) From 0e90d48e4eafde95804eec3be0db4953c0c67e15 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 27 Jul 2022 14:30:43 +0800 Subject: [PATCH 077/180] Revert "feat: add admin delete methods" This reverts commit c65c136f5507653d81801e98f28f60a9f69077b4. --- api/admin.go | 68 --------------------------------------- api/admin_test.go | 59 --------------------------------- api/api.go | 5 --- api/mfa.go | 6 ++-- models/audit_log_entry.go | 6 ---- models/recovery_code.go | 3 -- 6 files changed, 4 insertions(+), 143 deletions(-) diff --git a/api/admin.go b/api/admin.go index e277a547e..2134ce2d3 100644 --- a/api/admin.go +++ b/api/admin.go @@ -28,10 +28,6 @@ type adminUserParams struct { BanDuration string `json:"ban_duration"` } -type AdminUserDeleteFactorParams struct { - FactorID string `json:"factor_id"` -} - func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, error) { userID, err := uuid.FromString(chi.URLParam(r, "user_id")) if err != nil { @@ -346,67 +342,3 @@ func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error { return sendJSON(w, http.StatusOK, map[string]interface{}{}) } - -func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - - params := &AdminUserDeleteFactorParams{} - jsonDecoder := json.NewDecoder(r.Body) - err := jsonDecoder.Decode(params) - if err != nil { - return badRequestError("Invalid parameters: Please re-check request parameters: %v", err) - } - - factor, terr := models.FindFactorByFactorID(a.db, params.FactorID) - if terr != nil { - return terr - } - err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.FactorModifiedAction, r.RemoteAddr, map[string]interface{}{ - "user_id": user.ID, - "factor_id": factor.ID, - }); terr != nil { - return terr - } - if terr := tx.Destroy(factor); terr != nil { - return internalServerError("Database error deleting factor").WithInternalError(terr) - } - return nil - }) - if err != nil { - return err - } - return sendJSON(w, http.StatusOK, factor) - -} - -func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - - recoveryCodes, terr := models.FindValidRecoveryCodesByUser(a.db, user) - if terr != nil { - return terr - } - terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteRecoveryCodesAction, r.RemoteAddr, map[string]interface{}{ - "user_id": user.ID, - }); terr != nil { - return terr - } - for _, recoveryCodeModel := range recoveryCodes { - if terr := tx.Destroy(recoveryCodeModel); terr != nil { - return terr - } - } - return nil - }) - if terr != nil { - return terr - } - - return sendJSON(w, http.StatusOK, map[string]interface{}{}) -} diff --git a/api/admin_test.go b/api/admin_test.go index 269f207e5..b6e450c69 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -12,7 +12,6 @@ import ( "github.com/gofrs/uuid" jwt "github.com/golang-jwt/jwt" "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -598,61 +597,3 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { }) } } - -// TestAdminUserDelete tests API /admin/users//mfa/factor route (DELETE) -func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-factor-delete@example.com", "test", ts.Config.JWT.Aud, nil) - require.NoError(ts.T(), err, "Error making new user") - require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - - f, err := models.NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "") - require.NoError(ts.T(), err, "Error making new factor") - require.NoError(ts.T(), ts.API.db.Create(f), "Error creating factor") - - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "factor_id": f.ID, - })) - // Setup request - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/mfa/factor", u.ID), &buffer) - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) - - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) - _, err = models.FindFactorByFactorID(ts.API.db, f.ID) - require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) -} - -// TestAdminUserDelete tests API /admin/users//mfa/recovery_codes route (DELETE) -func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-factor-delete@example.com", "test", ts.Config.JWT.Aud, nil) - require.NoError(ts.T(), err, "Error making new user") - require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - - f, err := models.NewFactor(u, "A1B2C3", "testfactor-id", "totp", "disabled", "") - require.NoError(ts.T(), err, "Error making new factor") - require.NoError(ts.T(), ts.API.db.Create(f), "Error creating factor") - - for i := 0; i <= models.NumRecoveryCodes; i++ { - rc, err := models.NewRecoveryCode(u, crypto.SecureToken(models.RecoveryCodeLength)) - require.NoError(ts.T(), err) - err = ts.API.db.Create(rc) - require.NoError(ts.T(), err) - } - - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{})) - // Setup request - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/mfa/recovery_codes", u.ID), &buffer) - - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) - - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) - recoveryCodes, err := models.FindValidRecoveryCodesByUser(ts.API.db, u) - expectedRecoveryCodes := []*models.RecoveryCode{} - require.Equal(ts.T(), expectedRecoveryCodes, recoveryCodes) -} diff --git a/api/api.go b/api/api.go index 8d983a20a..1c0a53322 100644 --- a/api/api.go +++ b/api/api.go @@ -165,15 +165,10 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) - r.Route("/mfa", func(r *router) { - r.Delete("/factor", api.adminUserDeleteFactor) - r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) - }) r.Get("/", api.adminUserGet) r.Put("/", api.adminUserUpdate) r.Delete("/", api.adminUserDelete) - }) }) diff --git a/api/mfa.go b/api/mfa.go index 02e40187d..e37c6b7a6 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -60,6 +60,8 @@ type RecoveryCodesResponse struct { } func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { + const numRecoveryCodes = 8 + const recoveryCodeLength = 8 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) @@ -71,8 +73,8 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro var recoveryCode string var recoveryCodes []string var recoveryCodeModel *models.RecoveryCode - for i := 0; i < models.NumRecoveryCodes; i++ { - recoveryCode = crypto.SecureToken(models.RecoveryCodeLength) + for i := 0; i < numRecoveryCodes; i++ { + recoveryCode = crypto.SecureToken(recoveryCodeLength) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) if terr != nil { return internalServerError("Error creating recovery code").WithInternalError(terr) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index c9198984a..5d802a360 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -32,9 +32,6 @@ const ( EnrollFactorAction AuditAction = "factor_enrolled" CreateChallengeAction AuditAction = "challenge_created" VerifyFactorAction AuditAction = "verification_attempted" - DeleteFactorAction AuditAction = "factor_deleted" - FactorModifiedAction AuditAction = "factor_modified" - DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" account auditLogType = "account" team auditLogType = "team" @@ -60,9 +57,6 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ EnrollFactorAction: factor, CreateChallengeAction: factor, VerifyFactorAction: factor, - DeleteFactorAction: factor, - FactorModifiedAction: factor, - DeleteRecoveryCodesAction: team, } // AuditLogEntry is the database model for audit log entries. diff --git a/models/recovery_code.go b/models/recovery_code.go index 133140c3b..244935982 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -8,9 +8,6 @@ import ( "time" ) -const NumRecoveryCodes = 8 -const RecoveryCodeLength = 8 - type RecoveryCode struct { ID uuid.UUID `json:"id" db:"id"` UserID uuid.UUID `json:"user_id" db:"user_id"` From b50d0eb2e0e836529e490e9e8aa25444aca780a2 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 28 Jul 2022 14:29:45 +0800 Subject: [PATCH 078/180] refactr: move routes to /user --- api/admin.go | 15 ++++++++++ api/api.go | 24 ++++++++------- api/context.go | 15 ++++++++++ api/mfa.go | 55 ++++++---------------------------- api/mfa_test.go | 79 ++++++++++--------------------------------------- 5 files changed, 69 insertions(+), 119 deletions(-) diff --git a/api/admin.go b/api/admin.go index 2134ce2d3..3440bafe3 100644 --- a/api/admin.go +++ b/api/admin.go @@ -48,6 +48,21 @@ func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, return withUser(r.Context(), u), nil } +func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Context, error) { + factorID := chi.URLParam(r, "factor_id") + + logEntrySetField(r, "factor_id", factorID) + f, err := models.FindFactorByFactorID(a.db, factorID) + if err != nil { + if models.IsNotFoundError(err) { + return nil, notFoundError("Factor not found") + } + return nil, internalServerError("Database error loading factor").WithInternalError(err) + } + // write withFactor + return withFactor(r.Context(), f), nil +} + func (a *API) getAdminParams(r *http.Request) (*adminUserParams, error) { params := adminUserParams{} err := json.NewDecoder(r.Body).Decode(¶ms) diff --git a/api/api.go b/api/api.go index 1c0a53322..fb0b06fcc 100644 --- a/api/api.go +++ b/api/api.go @@ -150,6 +150,20 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.requireAuthentication) r.Get("/", api.UserGet) r.With(sharedLimiter).Put("/", api.UserUpdate) + r.Route("/{user_id}", func(r *router) { + r.Use(api.loadUser) + r.Route("/factor", func(r *router) { + r.Route("/{factor_id}", func(r *router) { + r.Use(api.loadFactor) + r.Post("/enroll", api.EnrollFactor) + r.Post("/verify", api.VerifyFactor) + r.Post("/challenge", api.ChallengeFactor) + + }) + }) + r.Post("/recovery_codes", api.GenerateRecoveryCodes) + }) + }) r.Route("/admin", func(r *router) { @@ -183,16 +197,6 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Get("/metadata", api.SAMLMetadata) }) - r.Route("/mfa", func(r *router) { - r.Route("/{user_id}", func(r *router) { - r.Use(api.loadUser) - r.Post("/recovery_codes", api.GenerateRecoveryCodes) - r.Post("/factor", api.EnrollFactor) - r.Post("/challenge", api.ChallengeFactor) - r.Post("/verify", api.VerifyFactor) - - }) - }) }) if globalConfig.MultiInstanceMode { diff --git a/api/context.go b/api/context.go index f1c4a78c1..e556e6a7d 100644 --- a/api/context.go +++ b/api/context.go @@ -26,6 +26,7 @@ const ( netlifyIDKey = contextKey("netlify_id") externalProviderTypeKey = contextKey("external_provider_type") userKey = contextKey("user") + factorKey = contextKey("factor") externalReferrerKey = contextKey("external_referrer") functionHooksKey = contextKey("function_hooks") adminUserKey = contextKey("admin_user") @@ -117,6 +118,11 @@ func withUser(ctx context.Context, u *models.User) context.Context { return context.WithValue(ctx, userKey, u) } +// with Factor adds the factor id to the context. +func withFactor(ctx context.Context, f *models.Factor) context.Context { + return context.WithValue(ctx, factorKey, f) +} + // getUser reads the user id from the context. func getUser(ctx context.Context) *models.User { obj := ctx.Value(userKey) @@ -126,6 +132,15 @@ func getUser(ctx context.Context) *models.User { return obj.(*models.User) } +// getFactor reads the factor id from the context +func getFactor(ctx context.Context) *models.Factor { + obj := ctx.Value(factorKey) + if obj == nil { + return nil + } + return obj.(*models.Factor) +} + // withSignature adds the provided request ID to the context. func withSignature(ctx context.Context, id string) context.Context { return context.WithValue(ctx, signatureKey, id) diff --git a/api/mfa.go b/api/mfa.go index e37c6b7a6..fc56f4f9a 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -33,11 +33,6 @@ type EnrollFactorResponse struct { TOTP TOTPObject } -type ChallengeFactorParams struct { - FactorID string `json:"factor_id"` - FriendlyName string `json:"friendly_name"` -} - type VerifyFactorParams struct { ChallengeID string `json:"challenge_id"` Code string `json:"code"` @@ -166,36 +161,11 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() config := a.getConfig(ctx) user := getUser(ctx) + factor := getFactor(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return forbiddenError(MFANotEnabledMsg) - } - var factor *models.Factor - var err error - - params := &ChallengeFactorParams{} - jsonDecoder := json.NewDecoder(r.Body) - err = jsonDecoder.Decode(params) - if err != nil { - return badRequestError("Could not read EnrollFactor params: %v", err) - } - factorID := params.FactorID - - if factorID != "" { - factor, err = models.FindFactorByFactorID(a.db, factorID) - } else { - return unprocessableEntityError("FactorID should be provided to create a challenge") - } - if err != nil { - if models.IsNotFoundError(err) { - return notFoundError(err.Error()) - } - return internalServerError("Database error finding factor").WithInternalError(err) - } - challenge, terr := models.NewChallenge(factor) if terr != nil { - return internalServerError("Database error creating challenge").WithInternalError(err) + return internalServerError("Database error creating challenge").WithInternalError(terr) } terr = a.db.Transaction(func(tx *storage.Connection) error { @@ -203,23 +173,23 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { return terr } if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ - "factor_id": params.FactorID, + "factor_id": factor.ID, "factor_status": factor.Status, }); terr != nil { return terr } - return nil }) - creationTime := challenge.CreatedAt - if err != nil { - return internalServerError("Error parsing database timestamp").WithInternalError(err) + if terr != nil { + return terr } + creationTime := challenge.CreatedAt + expiryTime := creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)) return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, CreatedAt: creationTime.String(), - ExpiresAt: creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)).String(), + ExpiresAt: expiryTime.String(), }) } @@ -228,6 +198,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() config := a.getConfig(ctx) user := getUser(ctx) + factor := getFactor(ctx) instanceID := getInstanceID(ctx) if !user.MFAEnabled { return forbiddenError(MFANotEnabledMsg) @@ -240,14 +211,6 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError("Please check the params passed into VerifyFactor: %v", err) } - factor, err := models.FindFactorByChallengeID(a.db, params.ChallengeID) - if err != nil { - if models.IsNotFoundError(err) { - return notFoundError(err.Error()) - } - return internalServerError("Database error finding factor").WithInternalError(err) - } - challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) if err != nil { if models.IsNotFoundError(err) { diff --git a/api/mfa_test.go b/api/mfa_test.go index 4b01d7635..0afe318f3 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -41,7 +41,6 @@ func TestMFA(t *testing.T) { func (ts *MFATestSuite) SetupTest() { models.TruncateAll(ts.API.db) - // Create user u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") @@ -61,7 +60,7 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/recovery_codes", user.ID), nil) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/recovery_codes", user.ID), nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) @@ -79,23 +78,13 @@ func (ts *MFATestSuite) TestEnrollFactor() { FriendlyName string FactorType string Issuer string - MFAEnabled bool expectedCode int }{ - { - "TOTP: MFA is disabled", - "", - "totp", - "supabase.com", - false, - http.StatusForbidden, - }, { "TOTP: Factor has friendly name", "bob", "totp", "supabase.com", - true, http.StatusOK, }, { @@ -103,7 +92,6 @@ func (ts *MFATestSuite) TestEnrollFactor() { "", "totp", "supabase.com", - true, http.StatusOK, }, } @@ -113,13 +101,12 @@ func (ts *MFATestSuite) TestEnrollFactor() { require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"friendly_name": c.FriendlyName, "factor_type": c.FactorType, "issuer": c.Issuer})) user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) - require.NoError(ts.T(), user.EnableMFA(ts.API.db)) token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/factor", user.ID), &buffer) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor", user.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") ts.API.handler.ServeHTTP(w, req) @@ -138,57 +125,23 @@ func (ts *MFATestSuite) TestEnrollFactor() { } func (ts *MFATestSuite) TestChallengeFactor() { - cases := []struct { - desc string - id string - mfaEnabled bool - expectedCode int - }{ - { - "MFA Not Enabled", - "", - false, - http.StatusForbidden, - }, - { - "Factor ID present", - "testFactorID", - true, - http.StatusOK, - }, - { - "Factor ID missing", - "", - true, - http.StatusUnprocessableEntity, - }, - } - for _, c := range cases { - ts.Run(c.desc, func() { - u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), err) + u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + require.NoError(ts.T(), err) - if c.mfaEnabled { - require.NoError(ts.T(), u.EnableMFA(ts.API.db), "Error setting MFA to disabled") - } + f, err := models.FindFactorByFactorID(ts.API.db, "testFactorID") + require.NoError(ts.T(), err) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err, "Error generating access token") + token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err, "Error generating access token") - var buffer bytes.Buffer - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "factor_id": c.id, - })) - - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/mfa/%s/challenge", u.ID), &buffer) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + var buffer bytes.Buffer + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost/user/%s/factor/%s/challenge", u.ID, f.ID), &buffer) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - w := httptest.NewRecorder() - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), c.expectedCode, w.Code) - }) - } + w := httptest.NewRecorder() + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) } func (ts *MFATestSuite) TestMFAVerifyFactor() { @@ -269,7 +222,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/verify", user.ID), &buffer) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/users/%s/factor/%s/verify", user.ID, f.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), v.expectedHTTPCode, w.Code) From 0c2c9fca7a8ddc580fdcd0dfaf406ec39af4de8c Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 28 Jul 2022 14:42:00 +0800 Subject: [PATCH 079/180] refactor: modify routes --- api/api.go | 2 +- api/mfa.go | 3 --- api/mfa_test.go | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/api/api.go b/api/api.go index fb0b06fcc..61b2ea97e 100644 --- a/api/api.go +++ b/api/api.go @@ -153,9 +153,9 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) r.Route("/factor", func(r *router) { + r.Post("/", api.EnrollFactor) r.Route("/{factor_id}", func(r *router) { r.Use(api.loadFactor) - r.Post("/enroll", api.EnrollFactor) r.Post("/verify", api.VerifyFactor) r.Post("/challenge", api.ChallengeFactor) diff --git a/api/mfa.go b/api/mfa.go index fc56f4f9a..22d3377e5 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -104,9 +104,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return forbiddenError(MFANotEnabledMsg) - } params := &EnrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) diff --git a/api/mfa_test.go b/api/mfa_test.go index 0afe318f3..bcb693c9d 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -106,7 +106,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor", user.ID), &buffer) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/", user.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") ts.API.handler.ServeHTTP(w, req) @@ -222,7 +222,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { require.NoError(ts.T(), err) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/users/%s/factor/%s/verify", user.ID, f.ID), &buffer) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/%s/verify", user.ID, f.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), v.expectedHTTPCode, w.Code) From b0e5dda8b88273690c1666ebfcba8de1b867a3b3 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 29 Jul 2022 15:22:24 +0800 Subject: [PATCH 080/180] chore: remove stray comment --- api/admin.go | 1 - 1 file changed, 1 deletion(-) diff --git a/api/admin.go b/api/admin.go index 3440bafe3..27700d52f 100644 --- a/api/admin.go +++ b/api/admin.go @@ -59,7 +59,6 @@ func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Contex } return nil, internalServerError("Database error loading factor").WithInternalError(err) } - // write withFactor return withFactor(r.Context(), f), nil } From 5374da8566093f0bed82cfa510f0a16e8bdb98d3 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 29 Jul 2022 15:56:53 +0800 Subject: [PATCH 081/180] chore: increase number of requests made so expected error is observed --- api/token_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/token_test.go b/api/token_test.go index 972702b87..c98daf849 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -64,7 +64,7 @@ func (ts *TokenTestSuite) TestRateLimitTokenRefresh() { req.Header.Set("My-Custom-Header", "1.2.3.4") // It rate limits after 30 requests - for i := 0; i < 30; i++ { + for i := 0; i < 35; i++ { w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) assert.Equal(ts.T(), http.StatusBadRequest, w.Code) From bb24b9abc95125e4c3b3344c38e0641959eb90aa Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 29 Jul 2022 15:59:47 +0800 Subject: [PATCH 082/180] Revert "chore: increase number of requests made so expected error is observed" This reverts commit 5374da8566093f0bed82cfa510f0a16e8bdb98d3. --- api/token_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/token_test.go b/api/token_test.go index c98daf849..972702b87 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -64,7 +64,7 @@ func (ts *TokenTestSuite) TestRateLimitTokenRefresh() { req.Header.Set("My-Custom-Header", "1.2.3.4") // It rate limits after 30 requests - for i := 0; i < 35; i++ { + for i := 0; i < 30; i++ { w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) assert.Equal(ts.T(), http.StatusBadRequest, w.Code) From 1a275ef0d12e8bd96b3f2d72d1076a121c9b3c2a Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 1 Aug 2022 10:08:25 +0800 Subject: [PATCH 083/180] chore: refactor MFA unenroll route --- api/api.go | 1 + api/mfa.go | 16 +++------------- api/mfa_test.go | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/api/api.go b/api/api.go index 61b2ea97e..adb0f6b7a 100644 --- a/api/api.go +++ b/api/api.go @@ -158,6 +158,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadFactor) r.Post("/verify", api.VerifyFactor) r.Post("/challenge", api.ChallengeFactor) + r.Delete("/", api.UnenrollFactor) }) }) diff --git a/api/mfa.go b/api/mfa.go index 01a3aa343..de4da3171 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -54,8 +54,7 @@ type UnenrollFactorResponse struct { } type UnenrollFactorParams struct { - FactorID string `json:"factor_id"` - Code string `json:"code"` + Code string `json:"code"` } // RecoveryCodesResponse represents a successful recovery code generation response @@ -273,10 +272,9 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { var err error ctx := r.Context() user := getUser(ctx) + factor := getFactor(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return forbiddenError(MFANotEnabledMsg) - } + params := &UnenrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) err = jsonDecoder.Decode(params) @@ -284,14 +282,6 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError(err.Error()) } - factor, err := models.FindFactorByFactorID(a.db, params.FactorID) - if err != nil { - if models.IsNotFoundError(err) { - return notFoundError(err.Error()) - } - return internalServerError("Database error finding factor").WithInternalError(err) - } - valid := totp.Validate(params.Code, factor.SecretKey) if valid != true { return unauthorizedError("Invalid code entered") diff --git a/api/mfa_test.go b/api/mfa_test.go index f6f7ded49..e51cbf37d 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -275,7 +275,7 @@ func (ts *MFATestSuite) TestUnenrollFactor() { })) w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/mfa/%s/unenroll", u.ID), &buffer) + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/user/%s/factor/%s/", u.ID, f.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) From 9f92e2eb672c3e0241734bde7b920b5be02bc837 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 1 Aug 2022 13:38:03 +0800 Subject: [PATCH 084/180] chore: add updated routes --- api/admin.go | 1 - api/api.go | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/admin.go b/api/admin.go index e277a547e..dc5bbca8d 100644 --- a/api/admin.go +++ b/api/admin.go @@ -351,7 +351,6 @@ func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) erro ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - params := &AdminUserDeleteFactorParams{} jsonDecoder := json.NewDecoder(r.Body) err := jsonDecoder.Decode(params) diff --git a/api/api.go b/api/api.go index 86e8b5621..d488a8edd 100644 --- a/api/api.go +++ b/api/api.go @@ -165,11 +165,13 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) - r.Route("/mfa", func(r *router) { - r.Delete("/factor", api.adminUserDeleteFactor) - r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) + r.Route("/factors", func(r *router) { + r.Use(api.loadFactor) + r.Get("/", adminUserGetFactors) + r.Delete("/{factor_id}", api.adminUserDeleteFactor) + r.Get("/{factor_id}", api.adminUserGetFactor) }) - + r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) r.Get("/", api.adminUserGet) r.Put("/", api.adminUserUpdate) r.Delete("/", api.adminUserDelete) From e62e00053985b4dec29d23c338ee025ea1a1e206 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 1 Aug 2022 22:52:23 +0800 Subject: [PATCH 085/180] feat: admin endpoints --- models/audit_log_entry.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 5d802a360..49bcb256f 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -32,12 +32,15 @@ const ( EnrollFactorAction AuditAction = "factor_enrolled" CreateChallengeAction AuditAction = "challenge_created" VerifyFactorAction AuditAction = "verification_attempted" - - account auditLogType = "account" - team auditLogType = "team" - token auditLogType = "token" - user auditLogType = "user" - factor auditLogType = "factor" + DeleteFactorAction AuditAction = "factor_deleted" + DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" + + account auditLogType = "account" + team auditLogType = "team" + token auditLogType = "token" + user auditLogType = "user" + factor auditLogType = "factor" + recoveryCodes auditLogType = "recovery_codes" ) var actionLogTypeMap = map[AuditAction]auditLogType{ @@ -57,6 +60,8 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ EnrollFactorAction: factor, CreateChallengeAction: factor, VerifyFactorAction: factor, + DeleteFactorAction: factor, + DeleteRecoveryCodesAction: recoveryCodes, } // AuditLogEntry is the database model for audit log entries. From 84fb5ba3e4100894af8ba92040893e503744d844 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 1 Aug 2022 22:58:32 +0800 Subject: [PATCH 086/180] fix: refactor and update audit log action types --- api/admin.go | 31 +++++++++++------- api/admin_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++ api/api.go | 12 ++++--- 3 files changed, 107 insertions(+), 17 deletions(-) diff --git a/api/admin.go b/api/admin.go index 928dd41b3..88d9fcd5e 100644 --- a/api/admin.go +++ b/api/admin.go @@ -361,19 +361,10 @@ func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) erro ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - params := &AdminUserDeleteFactorParams{} - jsonDecoder := json.NewDecoder(r.Body) - err := jsonDecoder.Decode(params) - if err != nil { - return badRequestError("Invalid parameters: Please re-check request parameters: %v", err) - } + factor := getFactor(ctx) - factor, terr := models.FindFactorByFactorID(a.db, params.FactorID) - if terr != nil { - return terr - } - err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.FactorModifiedAction, r.RemoteAddr, map[string]interface{}{ + err := a.db.Transaction(func(tx *storage.Connection) error { + if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "factor_id": factor.ID, }); terr != nil { @@ -419,3 +410,19 @@ func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Reques return sendJSON(w, http.StatusOK, map[string]interface{}{}) } + +func (a *API) adminUserGetFactors(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + user := getUser(ctx) + factors, terr := models.FindFactorsByUser(a.db, user) + if terr != nil { + return terr + } + return sendJSON(w, http.StatusOK, factors) +} + +// Returns information about a single factor +func (a *API) adminUserGetFactor(w http.ResponseWriter, r *http.Request) error { + factor := getFactor(r.Context()) + return sendJSON(w, http.StatusOK, factor) +} diff --git a/api/admin_test.go b/api/admin_test.go index b6e450c69..3551b6f92 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -597,3 +597,84 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { }) } } + +// TestAdminUserDeleteRecoveryCodes tests API /admin/users//recovery_codes/ +func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { + u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + //TODO: Create Recovery Codes + + // Setup request + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/recovery_codes", u.ID), nil) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) +} + +// TestAdminUserDeleteFactor tests API /admin/users//factor// +func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { + // TODO: Refactor this + u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + + // Setup request + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), nil) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + +} + +// TestAdminUserGetFactor tests API /admin/user//factors/ +func (ts *AdminTestSuite) TestAdminUserGetFactors() { + u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + + // Setup request + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s/factor/", u.ID), nil) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) +} + +// TestAdminUserGetFactor tests API /admin/user//factors/ +func (ts *AdminTestSuite) TestAdminUserGetFactor() { + u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + + // Setup request + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), nil) + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + +} diff --git a/api/api.go b/api/api.go index 5be1beff0..f902af748 100644 --- a/api/api.go +++ b/api/api.go @@ -179,11 +179,13 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Route("/{user_id}", func(r *router) { r.Use(api.loadUser) - r.Route("/factors", func(r *router) { - r.Use(api.loadFactor) - r.Get("/", adminUserGetFactors) - r.Delete("/{factor_id}", api.adminUserDeleteFactor) - r.Get("/{factor_id}", api.adminUserGetFactor) + r.Route("/factor", func(r *router) { + r.Get("/", api.adminUserGetFactors) + r.Route("/{factor_id}", func(r *router) { + r.Use(api.loadFactor) + r.Delete("/", api.adminUserDeleteFactor) + r.Get("/", api.adminUserGetFactor) + }) }) r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) From 3e08ffb43aafd69cb52f38e05f000ad7864faecb Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 1 Aug 2022 23:23:36 +0800 Subject: [PATCH 087/180] tests: add additional checks for deletion endpoints --- api/admin_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/api/admin_test.go b/api/admin_test.go index 3551b6f92..f9a5d2a66 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -12,6 +12,7 @@ import ( "github.com/gofrs/uuid" jwt "github.com/golang-jwt/jwt" "github.com/netlify/gotrue/conf" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -600,11 +601,18 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { // TestAdminUserDeleteRecoveryCodes tests API /admin/users//recovery_codes/ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { + numRecoveryCodes := 8 + recoveryCodeLength := 8 u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - //TODO: Create Recovery Codes + // Create batch of Recovery Codes + for i := 0; i < numRecoveryCodes; i++ { + r, terr := models.NewRecoveryCode(u, crypto.SecureToken(recoveryCodeLength)) + require.NoError(ts.T(), terr, "Error creating recovery code model") + require.NoError(ts.T(), ts.API.db.Create(r), "Error creating recovery code") + } // Setup request w := httptest.NewRecorder() @@ -614,6 +622,10 @@ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) + // No valid recovery codes as recovery codes are generated in batches. + rc, err := models.FindValidRecoveryCodesByUser(ts.API.db, u) + require.Equal(ts.T(), 0, len(rc)) + } // TestAdminUserDeleteFactor tests API /admin/users//factor// @@ -636,6 +648,9 @@ func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) + _, err = models.FindFactorByFactorID(ts.API.db, f.ID) + require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) + } // TestAdminUserGetFactor tests API /admin/user//factors/ From 31137e4db4c299c93292b2ba675c50fc06e27993 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 2 Aug 2022 10:26:09 +0800 Subject: [PATCH 088/180] refactor: remove stray constants --- api/admin_test.go | 6 ++---- api/mfa.go | 13 +++++-------- api/mfa_test.go | 3 +-- models/mfa_constants.go | 4 ++++ models/recovery_code_test.go | 9 +++------ 5 files changed, 15 insertions(+), 20 deletions(-) create mode 100644 models/mfa_constants.go diff --git a/api/admin_test.go b/api/admin_test.go index f9a5d2a66..025a21bc2 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -601,15 +601,13 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { // TestAdminUserDeleteRecoveryCodes tests API /admin/users//recovery_codes/ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { - numRecoveryCodes := 8 - recoveryCodeLength := 8 u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") // Create batch of Recovery Codes - for i := 0; i < numRecoveryCodes; i++ { - r, terr := models.NewRecoveryCode(u, crypto.SecureToken(recoveryCodeLength)) + for i := 0; i < models.NumRecoveryCodes; i++ { + r, terr := models.NewRecoveryCode(u, crypto.SecureToken(models.RecoveryCodeLength)) require.NoError(ts.T(), terr, "Error creating recovery code model") require.NoError(ts.T(), ts.API.db.Create(r), "Error creating recovery code") } diff --git a/api/mfa.go b/api/mfa.go index de4da3171..d30785e2a 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -63,21 +63,16 @@ type RecoveryCodesResponse struct { } func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { - const numRecoveryCodes = 8 - const recoveryCodeLength = 8 ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return forbiddenError(MFANotEnabledMsg) - } recoveryCodeModels := []*models.RecoveryCode{} var terr error var recoveryCode string var recoveryCodes []string var recoveryCodeModel *models.RecoveryCode - for i := 0; i < numRecoveryCodes; i++ { - recoveryCode = crypto.SecureToken(recoveryCodeLength) + for i := 0; i < models.NumRecoveryCodes; i++ { + recoveryCode = crypto.SecureToken(models.RecoveryCodeLength) recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) if terr != nil { return internalServerError("Error creating recovery code").WithInternalError(terr) @@ -138,7 +133,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) - // TODO(Joel): Convert constants into an Enum in future factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, models.FactorDisabledState, key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) @@ -299,6 +293,9 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { } return nil }) + if err != nil { + return err + } return sendJSON(w, http.StatusOK, &UnenrollFactorResponse{ Success: fmt.Sprintf("%v", valid), diff --git a/api/mfa_test.go b/api/mfa_test.go index e51cbf37d..6bd42ae98 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -28,7 +28,6 @@ type MFATestSuite struct { func TestMFA(t *testing.T) { api, config, instanceID, err := setupAPIForTestForInstance() require.NoError(t, err) - ts := &MFATestSuite{ API: api, Config: config, @@ -45,6 +44,7 @@ func (ts *MFATestSuite) SetupTest() { u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") + // Create Factor f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") @@ -268,7 +268,6 @@ func (ts *MFATestSuite) TestUnenrollFactor() { code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) require.NoError(ts.T(), err) - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ "factor_id": f.ID, "code": code, diff --git a/models/mfa_constants.go b/models/mfa_constants.go new file mode 100644 index 000000000..5df611b01 --- /dev/null +++ b/models/mfa_constants.go @@ -0,0 +1,4 @@ +package models + +const NumRecoveryCodes = 8 +const RecoveryCodeLength = 8 diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 6dd5848d8..82993f346 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -38,19 +38,17 @@ func TestRecoveryCode(t *testing.T) { } func (ts *RecoveryCodeTestSuite) TestFindValidRecoveryCodesByUser() { - // TODO: Joel -- convert numRecoveryCodes and recoveryCodeLength into constants in mfa.go - numRecoveryCodes := 8 var expectedRecoveryCodes []string user, err := NewUser(uuid.Nil, "", "", "", "", nil) err = ts.db.Create(user) require.NoError(ts.T(), err) - for i := 0; i <= numRecoveryCodes; i++ { + for i := 0; i <= NumRecoveryCodes; i++ { rc := ts.createRecoveryCode(user) expectedRecoveryCodes = append(expectedRecoveryCodes, rc.RecoveryCode) } recoveryCodes, err := FindValidRecoveryCodesByUser(ts.db, user) require.NoError(ts.T(), err) - require.Equal(ts.T(), numRecoveryCodes, len(recoveryCodes), fmt.Sprintf("Expected %d recovery codes but got %d", numRecoveryCodes, len(recoveryCodes))) + require.Equal(ts.T(), NumRecoveryCodes, len(recoveryCodes), fmt.Sprintf("Expected %d recovery codes but got %d", NumRecoveryCodes, len(recoveryCodes))) for index, recoveryCode := range recoveryCodes { require.Equal(ts.T(), expectedRecoveryCodes[index], recoveryCode, "Recovery codes should match") @@ -58,8 +56,7 @@ func (ts *RecoveryCodeTestSuite) TestFindValidRecoveryCodesByUser() { } func (ts *RecoveryCodeTestSuite) createRecoveryCode(u *User) *RecoveryCode { - recoveryCodeLength := 8 - rc, err := NewRecoveryCode(u, crypto.SecureToken(recoveryCodeLength)) + rc, err := NewRecoveryCode(u, crypto.SecureToken(RecoveryCodeLength)) require.NoError(ts.T(), err) err = ts.db.Create(rc) require.NoError(ts.T(), err) From ce941f9f248f6f9c8007f7c7f71b95f6cafc0633 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 2 Aug 2022 10:30:13 +0800 Subject: [PATCH 089/180] refactor: remove stray lines and comments --- api/admin.go | 1 - api/admin_test.go | 3 --- 2 files changed, 4 deletions(-) diff --git a/api/admin.go b/api/admin.go index 88d9fcd5e..268cbcabe 100644 --- a/api/admin.go +++ b/api/admin.go @@ -379,7 +379,6 @@ func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) erro return err } return sendJSON(w, http.StatusOK, factor) - } func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Request) error { diff --git a/api/admin_test.go b/api/admin_test.go index 025a21bc2..768ebe9e9 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -623,12 +623,10 @@ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { // No valid recovery codes as recovery codes are generated in batches. rc, err := models.FindValidRecoveryCodesByUser(ts.API.db, u) require.Equal(ts.T(), 0, len(rc)) - } // TestAdminUserDeleteFactor tests API /admin/users//factor// func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { - // TODO: Refactor this u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -689,5 +687,4 @@ func (ts *AdminTestSuite) TestAdminUserGetFactor() { ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) - } From 77aac52f8dbd80011809aa17d0db5283c02424dc Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 2 Aug 2022 10:37:26 +0800 Subject: [PATCH 090/180] refactor: remove notion of MFAEnabled --- api/mfa.go | 6 ------ api/mfa_test.go | 5 ----- api/signup_test.go | 2 +- migrations/20220607041349_add_mfa_schema.up.sql | 4 ---- models/user.go | 10 ---------- models/user_test.go | 12 ------------ 6 files changed, 1 insertion(+), 38 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index de4da3171..82af98730 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -68,9 +68,6 @@ func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) erro ctx := r.Context() user := getUser(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return forbiddenError(MFANotEnabledMsg) - } recoveryCodeModels := []*models.RecoveryCode{} var terr error var recoveryCode string @@ -205,9 +202,6 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { user := getUser(ctx) factor := getFactor(ctx) instanceID := getInstanceID(ctx) - if !user.MFAEnabled { - return forbiddenError(MFANotEnabledMsg) - } params := &VerifyFactorParams{} jsonDecoder := json.NewDecoder(r.Body) diff --git a/api/mfa_test.go b/api/mfa_test.go index e51cbf37d..5601e25ce 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -54,7 +54,6 @@ func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { const expectedNumOfRecoveryCodes = 8 user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) - require.NoError(ts.T(), user.EnableMFA(ts.API.db)) token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) @@ -172,9 +171,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { } for _, v := range cases { ts.Run(v.desc, func() { - // Create a User with MFA enabled u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), u.EnableMFA(ts.API.db)) emailValue, err := u.Email.Value() require.NoError(ts.T(), err) testEmail := emailValue.(string) @@ -242,9 +239,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { } func (ts *MFATestSuite) TestUnenrollFactor() { - // Create a User with MFA enabled u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - require.NoError(ts.T(), u.EnableMFA(ts.API.db)) emailValue, err := u.Email.Value() require.NoError(ts.T(), err) testEmail := emailValue.(string) diff --git a/api/signup_test.go b/api/signup_test.go index 96e6c67b3..a5327dece 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -78,7 +78,7 @@ func (ts *SignupTestSuite) TestSignup() { } func (ts *SignupTestSuite) TestWebhookTriggered() { - const numUserFields = 12 + const numUserFields = 11 var callCount int require := ts.Require() assert := ts.Assert() diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220607041349_add_mfa_schema.up.sql index c3c3c7be6..61840e525 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220607041349_add_mfa_schema.up.sql @@ -44,7 +44,3 @@ CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( CONSTRAINT mfa_recovery_codes_user_id_fkey FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); comment on table auth.mfa_recovery_codes is 'Auth: stores recovery codes for Multi Factor Authentication'; - --- Add MFA toggle on Users table -ALTER TABLE auth.users -ADD COLUMN IF NOT EXISTS mfa_enabled boolean NOT NULL DEFAULT FALSE; diff --git a/models/user.go b/models/user.go index 5d6dbb83d..0cb5672f3 100644 --- a/models/user.go +++ b/models/user.go @@ -67,7 +67,6 @@ type User struct { CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` BannedUntil *time.Time `json:"banned_until,omitempty" db:"banned_until"` - MFAEnabled bool `json:"mfa_enabled" db:"mfa_enabled"` } // NewUser initializes a new user from an email, password and user data. @@ -525,12 +524,3 @@ func (u *User) UpdateBannedUntil(tx *storage.Connection) error { return tx.UpdateOnly(u, "banned_until") } -func (u *User) EnableMFA(tx *storage.Connection) error { - u.MFAEnabled = true - return tx.UpdateOnly(u, "mfa_enabled") -} - -func (u *User) DisableMFA(tx *storage.Connection) error { - u.MFAEnabled = false - return tx.UpdateOnly(u, "mfa_enabled") -} diff --git a/models/user_test.go b/models/user_test.go index a1d89f688..ebc1bc3f2 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -43,18 +43,6 @@ func TestUser(t *testing.T) { suite.Run(t, ts) } -func (ts *UserTestSuite) TestToggleMFA() { - u, err := NewUser(uuid.Nil, "", "", "", "", nil) - require.NoError(ts.T(), err) - require.False(ts.T(), u.MFAEnabled) - - require.NoError(ts.T(), u.EnableMFA(ts.db)) - require.True(ts.T(), u.MFAEnabled) - - require.NoError(ts.T(), u.DisableMFA(ts.db)) - require.False(ts.T(), u.MFAEnabled) -} - func (ts *UserTestSuite) TestUpdateAppMetadata() { u, err := NewUser(uuid.Nil, "", "", "", "", nil) require.NoError(ts.T(), err) From a4022b1122a2ad0b1b18fd1d29ce331b62690078 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 2 Aug 2022 11:18:52 +0800 Subject: [PATCH 091/180] refactor: remove /recovery_codes endpoint --- api/api.go | 1 - api/mfa.go | 46 ---------------------------------------------- api/mfa_test.go | 21 --------------------- 3 files changed, 68 deletions(-) diff --git a/api/api.go b/api/api.go index adb0f6b7a..196c9347d 100644 --- a/api/api.go +++ b/api/api.go @@ -162,7 +162,6 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati }) }) - r.Post("/recovery_codes", api.GenerateRecoveryCodes) }) }) diff --git a/api/mfa.go b/api/mfa.go index 82af98730..461608e1d 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -57,52 +57,6 @@ type UnenrollFactorParams struct { Code string `json:"code"` } -// RecoveryCodesResponse represents a successful recovery code generation response -type RecoveryCodesResponse struct { - RecoveryCodes []string `json:"recovery_codes"` -} - -func (a *API) GenerateRecoveryCodes(w http.ResponseWriter, r *http.Request) error { - const numRecoveryCodes = 8 - const recoveryCodeLength = 8 - ctx := r.Context() - user := getUser(ctx) - instanceID := getInstanceID(ctx) - recoveryCodeModels := []*models.RecoveryCode{} - var terr error - var recoveryCode string - var recoveryCodes []string - var recoveryCodeModel *models.RecoveryCode - for i := 0; i < numRecoveryCodes; i++ { - recoveryCode = crypto.SecureToken(recoveryCodeLength) - recoveryCodeModel, terr = models.NewRecoveryCode(user, recoveryCode) - if terr != nil { - return internalServerError("Error creating recovery code").WithInternalError(terr) - } - recoveryCodes = append(recoveryCodes, recoveryCode) - recoveryCodeModels = append(recoveryCodeModels, recoveryCodeModel) - } - terr = a.db.Transaction(func(tx *storage.Connection) error { - for _, recoveryCodeModel := range recoveryCodeModels { - if terr = tx.Create(recoveryCodeModel); terr != nil { - return terr - } - } - - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.GenerateRecoveryCodesAction, r.RemoteAddr, nil); terr != nil { - return terr - } - return nil - }) - if terr != nil { - return terr - } - - return sendJSON(w, http.StatusOK, &RecoveryCodesResponse{ - RecoveryCodes: recoveryCodes, - }) -} - func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { const factorPrefix = "factor" const imageSideLength = 300 diff --git a/api/mfa_test.go b/api/mfa_test.go index 5601e25ce..b2d00b1b5 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -50,27 +50,6 @@ func (ts *MFATestSuite) SetupTest() { require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") } -func (ts *MFATestSuite) TestMFARecoveryCodeGeneration() { - const expectedNumOfRecoveryCodes = 8 - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) - ts.Require().NoError(err) - - token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err) - - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/recovery_codes", user.ID), nil) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) - - data := make(map[string]interface{}) - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - - recoveryCodes := data["recovery_codes"].([]interface{}) - require.Equal(ts.T(), expectedNumOfRecoveryCodes, len(recoveryCodes)) -} - func (ts *MFATestSuite) TestEnrollFactor() { var cases = []struct { desc string From 65b2424122735ee559a367e4975785577ada258f Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 3 Aug 2022 15:47:13 +0800 Subject: [PATCH 092/180] feat: add update factor admin endpoint --- api/admin.go | 59 +++++++++++++++++++++++++++++++++++++++ api/admin_test.go | 54 +++++++++++++++++++++++++++++++++++ api/api.go | 1 + models/audit_log_entry.go | 2 ++ models/factor.go | 6 ++++ 5 files changed, 122 insertions(+) diff --git a/api/admin.go b/api/admin.go index 268cbcabe..dad8571c4 100644 --- a/api/admin.go +++ b/api/admin.go @@ -28,6 +28,12 @@ type adminUserParams struct { BanDuration string `json:"ban_duration"` } +type adminUserUpdateFactorParams struct { + FriendlyName string `json:"friendly_name"` + FactorType string `json:"factor_type"` + FactorStatus string `json:"factor_status"` +} + func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, error) { userID, err := uuid.FromString(chi.URLParam(r, "user_id")) if err != nil { @@ -425,3 +431,56 @@ func (a *API) adminUserGetFactor(w http.ResponseWriter, r *http.Request) error { factor := getFactor(r.Context()) return sendJSON(w, http.StatusOK, factor) } + +// adminUserUpdate updates a single factor object +func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + factor := getFactor(ctx) + user := getUser(ctx) + adminUser := getAdminUser(ctx) + instanceID := getInstanceID(ctx) + + params := &adminUserUpdateFactorParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Please check the params passed into admin user update factor: %v", err) + } + + err = a.db.Transaction(func(tx *storage.Connection) error { + if params.FriendlyName != "" { + if terr := factor.UpdateFriendlyName(tx, params.FriendlyName); terr != nil { + return terr + } + } + if params.FactorType != "" { + // TODO(Joel): Update this to check factorType validity when we introduce webauthn + if terr := factor.UpdateFactorType(tx, params.FactorType); terr != nil { + return terr + } + } + if params.FactorStatus != "" { + if !isValidFactorStatus(params.FactorType) { + return errors.New("Factor Status should be one of the valid factor states: verified, unverified or disabled") + } + if terr := factor.UpdateStatus(tx, params.FactorStatus); terr != nil { + return terr + } + } + + if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UpdateFactorAction, "", map[string]interface{}{ + "user_id": user.ID, + "factor_id": factor.ID, + "factor_type": factor.FactorType, + }); terr != nil { + return terr + } + return nil + }) + + return sendJSON(w, http.StatusOK, factor) +} + +func isValidFactorStatus(factorStatus string) bool { + return factorStatus == models.FactorVerifiedState || factorStatus == models.FactorUnverifiedState || factorStatus == models.FactorDisabledState +} diff --git a/api/admin_test.go b/api/admin_test.go index 768ebe9e9..9bd3f6810 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -688,3 +688,57 @@ func (ts *AdminTestSuite) TestAdminUserGetFactor() { ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), http.StatusOK, w.Code) } + +func (ts *AdminTestSuite) TestAdminUserUpdateFactor() { + u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + require.NoError(ts.T(), err, "Error making new user") + require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + + var cases = []struct { + desc string + factorData map[string]interface{} + expected int + }{ + { + "Update Factor friendly name", + map[string]interface{}{ + "friendly_name": "john", + }, + http.StatusOK, + }, + { + "Update factor type", + map[string]interface{}{ + "friendly_name": "john", + "factor_type": "totp", + "factor_status": "unverified", + }, + http.StatusOK, + }, + { + "Update Factor Status", + map[string]interface{}{ + "factor_status": models.FactorVerifiedState, + }, + http.StatusOK, + }, + } + + // Initialize factor data + for _, c := range cases { + ts.Run(c.desc, func() { + var buffer bytes.Buffer + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(c.factorData)) + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/admin/users/%s/factor/%s/", u.ID, f.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ts.token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), http.StatusOK, w.Code) + }) + } + +} diff --git a/api/api.go b/api/api.go index 28721f6cc..73d556c86 100644 --- a/api/api.go +++ b/api/api.go @@ -185,6 +185,7 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadFactor) r.Delete("/", api.adminUserDeleteFactor) r.Get("/", api.adminUserGetFactor) + r.Post("/", api.adminUserUpdateFactor) }) }) r.Delete("/recovery_codes", api.adminUserDeleteRecoveryCodes) diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index d75eb94db..aa83918b5 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -35,6 +35,7 @@ const ( VerifyFactorAction AuditAction = "verification_attempted" DeleteFactorAction AuditAction = "factor_deleted" DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" + UpdateFactorAction AuditAction = "factor_updated" account auditLogType = "account" team auditLogType = "team" @@ -63,6 +64,7 @@ var actionLogTypeMap = map[AuditAction]auditLogType{ CreateChallengeAction: factor, VerifyFactorAction: factor, DeleteFactorAction: factor, + UpdateFactorAction: factor, DeleteRecoveryCodesAction: recoveryCodes, } diff --git a/models/factor.go b/models/factor.go index f7468ca98..04c2b1fc1 100644 --- a/models/factor.go +++ b/models/factor.go @@ -103,3 +103,9 @@ func (f *Factor) UpdateStatus(tx *storage.Connection, status string) error { f.Status = status return tx.UpdateOnly(f, "status", "updated_at") } + +// Change the factor type +func (f *Factor) UpdateFactorType(tx *storage.Connection, factorType string) error { + f.FactorType = factorType + return tx.UpdateOnly(f, "factor_type", "updated_at") +} From ac845bab80f8114053a64050da6b394cfa1d4bba Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 3 Aug 2022 20:41:29 +0800 Subject: [PATCH 093/180] feat: add IsMFAEnabled --- models/factor.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/models/factor.go b/models/factor.go index f7468ca98..331a1a415 100644 --- a/models/factor.go +++ b/models/factor.go @@ -92,6 +92,17 @@ func FindFactorByChallengeID(tx *storage.Connection, challengeID string) (*Facto return factor, nil } +func FindVerifiedFactorsByUser(tx *storage.Connection, user *User) ([]*Factor, error) { + factors := []*Factor{} + if err := tx.Q().Where("user_id = ? AND status = ?", user.ID, FactorVerifiedState).All(&factors); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return factors, nil + } + return nil, errors.Wrap(err, "Error finding verified mfa factors") + } + return factors, nil +} + // Change the friendly name func (f *Factor) UpdateFriendlyName(tx *storage.Connection, friendlyName string) error { f.FriendlyName = friendlyName @@ -103,3 +114,15 @@ func (f *Factor) UpdateStatus(tx *storage.Connection, status string) error { f.Status = status return tx.UpdateOnly(f, "status", "updated_at") } + +// Checks if MFA is Enabled +func IsMFAEnabled(tx *storage.Connection, user *User) (bool, error) { + factors, err := FindVerifiedFactorsByUser(tx, user) + if err != nil { + return false, err + } + if len(factors) >= 1 { + return true, nil + } + return false, nil +} From a0eeee9ea5e44cfa7481b4510611317ba4fbc347 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 3 Aug 2022 20:57:37 +0800 Subject: [PATCH 094/180] refactor: reinstate MFAEnabled --- api/admin.go | 16 +++++++++++++++- api/admin_test.go | 8 +++++++- api/mfa.go | 6 ++++++ api/mfa_test.go | 2 ++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/api/admin.go b/api/admin.go index 268cbcabe..fac5c3d87 100644 --- a/api/admin.go +++ b/api/admin.go @@ -363,7 +363,14 @@ func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) erro instanceID := getInstanceID(ctx) factor := getFactor(ctx) - err := a.db.Transaction(func(tx *storage.Connection) error { + MFAEnabled, err := models.IsMFAEnabled(a.db, user) + if err != nil { + return err + } else if !MFAEnabled { + return forbiddenError("You do not have a verified factor enrolled") + } + + err = a.db.Transaction(func(tx *storage.Connection) error { if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "factor_id": factor.ID, @@ -386,6 +393,13 @@ func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Reques user := getUser(ctx) instanceID := getInstanceID(ctx) + MFAEnabled, err := models.IsMFAEnabled(a.db, user) + if err != nil { + return err + } else if !MFAEnabled { + return forbiddenError("You do not have a verified factor enrolled") + } + recoveryCodes, terr := models.FindValidRecoveryCodesByUser(a.db, user) if terr != nil { return terr diff --git a/api/admin_test.go b/api/admin_test.go index 768ebe9e9..09c746619 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -601,10 +601,15 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { // TestAdminUserDeleteRecoveryCodes tests API /admin/users//recovery_codes/ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { + // TODO(Joel): Test case where factor is unverified u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorVerifiedState, "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") + require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") + // Create batch of Recovery Codes for i := 0; i < models.NumRecoveryCodes; i++ { r, terr := models.NewRecoveryCode(u, crypto.SecureToken(models.RecoveryCodeLength)) @@ -627,11 +632,12 @@ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { // TestAdminUserDeleteFactor tests API /admin/users//factor// func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { + // TODO(Joel): Test case where factor is unverified u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorVerifiedState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") diff --git a/api/mfa.go b/api/mfa.go index 7037c499e..0e8425b75 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -228,6 +228,12 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { if err != nil { return badRequestError(err.Error()) } + MFAEnabled, err := models.IsMFAEnabled(a.db, user) + if err != nil { + return err + } else if !MFAEnabled { + return forbiddenError("You do not have a verified factor enrolled") + } valid := totp.Validate(params.Code, factor.SecretKey) if valid != true { diff --git a/api/mfa_test.go b/api/mfa_test.go index bea0117ca..06bae4d6d 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -218,6 +218,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { } func (ts *MFATestSuite) TestUnenrollFactor() { + // TODO(Joel): Test case where factor is unverified u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) emailValue, err := u.Email.Value() require.NoError(ts.T(), err) @@ -232,6 +233,7 @@ func (ts *MFATestSuite) TestUnenrollFactor() { factors, err := models.FindFactorsByUser(ts.API.db, u) f := factors[0] f.SecretKey = sharedSecret + err = f.UpdateStatus(ts.API.db, models.FactorVerifiedState) require.NoError(ts.T(), err) require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") From a4af59ff513d6a4c7fcabc2c4fbf762a99a9d354 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sat, 6 Aug 2022 18:30:45 +0800 Subject: [PATCH 095/180] refactor: make issuer mandatory --- api/mfa.go | 6 ++++-- api/mfa_test.go | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 7037c499e..3983b33d4 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -41,7 +41,6 @@ type VerifyFactorParams struct { type ChallengeFactorResponse struct { ID string `json:"id"` CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` ExpiresAt string `json:"expires_at"` } @@ -73,6 +72,9 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if (params.FactorType != "totp") && (params.FactorType != "webauthn") { return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") } + if params.Issuer == "" { + return unprocessableEntityError("Issuer is required") + } // TODO(Joel): Review this portion when email is no longer a primary key key, err := totp.Generate(totp.GenerateOpts{ Issuer: params.Issuer, @@ -190,7 +192,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { err = a.db.Transaction(func(tx *storage.Connection) error { if err = models.NewAuditLogEntry(tx, instanceID, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ "factor_id": factor.ID, - "challenge_id": params.ChallengeID, + "challenge_id": challenge.ID, }); err != nil { return err } diff --git a/api/mfa_test.go b/api/mfa_test.go index bea0117ca..cb17b1490 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -56,7 +56,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { FriendlyName string FactorType string Issuer string - expectedCode int + ExpectedCode int }{ { "TOTP: Factor has friendly name", @@ -72,6 +72,20 @@ func (ts *MFATestSuite) TestEnrollFactor() { "supabase.com", http.StatusOK, }, + { + "TOTP: No issuer", + "john", + "totp", + "", + http.StatusUnprocessableEntity, + }, + { + "Invalid factor type", + "bob", + "", + "john.com", + http.StatusUnprocessableEntity, + }, } for _, c := range cases { ts.Run(c.desc, func() { @@ -88,13 +102,13 @@ func (ts *MFATestSuite) TestEnrollFactor() { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) + require.Equal(ts.T(), c.ExpectedCode, w.Code) factors, err := models.FindFactorsByUser(ts.API.db, user) ts.Require().NoError(err) latestFactor := factors[len(factors)-1] require.Equal(ts.T(), models.FactorDisabledState, latestFactor.Status) - if c.FriendlyName != "" { + if c.FriendlyName != "" && c.ExpectedCode == http.StatusOK { require.Equal(ts.T(), c.FriendlyName, latestFactor.FriendlyName) } }) From 1da78f2eb31d74593e227037c249f4f0252e1d8a Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 10 Aug 2022 14:24:17 +0800 Subject: [PATCH 096/180] chore: remove created_at field from /challenge --- api/mfa.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 7037c499e..0b88294e6 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -40,7 +40,6 @@ type VerifyFactorParams struct { type ChallengeFactorResponse struct { ID string `json:"id"` - CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` ExpiresAt string `json:"expires_at"` } @@ -143,7 +142,6 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { expiryTime := creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)) return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, - CreatedAt: creationTime.String(), ExpiresAt: expiryTime.String(), }) } From 96bf6fb93b574b080917e4d8a471a59854eea0b4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Sun, 28 Aug 2022 18:45:34 +0800 Subject: [PATCH 097/180] chore: merge in master --- api/admin.go | 12 ++++-------- api/admin_test.go | 10 +++++----- api/context.go | 2 ++ api/mfa.go | 28 ++++++++++++++-------------- api/mfa_test.go | 33 +++++++++++++++------------------ api/signup_test.go | 2 +- go.mod | 6 ++++-- models/challenge_test.go | 3 +-- models/factor_test.go | 7 +++---- models/instance.go | 8 ++++---- models/recovery_code_test.go | 3 +-- models/user.go | 2 +- 12 files changed, 55 insertions(+), 61 deletions(-) diff --git a/api/admin.go b/api/admin.go index 5e97146b3..18eb9418e 100644 --- a/api/admin.go +++ b/api/admin.go @@ -57,7 +57,7 @@ func (a *API) loadUser(w http.ResponseWriter, r *http.Request) (context.Context, func (a *API) loadFactor(w http.ResponseWriter, r *http.Request) (context.Context, error) { factorID := chi.URLParam(r, "factor_id") - logEntrySetField(r, "factor_id", factorID) + logger.LogEntrySetField(r, "factor_id", factorID) f, err := models.FindFactorByFactorID(a.db, factorID) if err != nil { if models.IsNotFoundError(err) { @@ -375,7 +375,6 @@ func (a *API) adminUserDelete(w http.ResponseWriter, r *http.Request) error { func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) - instanceID := getInstanceID(ctx) factor := getFactor(ctx) MFAEnabled, err := models.IsMFAEnabled(a.db, user) @@ -386,7 +385,7 @@ func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) erro } err = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{ + if terr := models.NewAuditLogEntry(r, tx, user, models.DeleteFactorAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "factor_id": factor.ID, }); terr != nil { @@ -406,7 +405,6 @@ func (a *API) adminUserDeleteFactor(w http.ResponseWriter, r *http.Request) erro func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) - instanceID := getInstanceID(ctx) MFAEnabled, err := models.IsMFAEnabled(a.db, user) if err != nil { @@ -420,7 +418,7 @@ func (a *API) adminUserDeleteRecoveryCodes(w http.ResponseWriter, r *http.Reques return terr } terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.DeleteRecoveryCodesAction, r.RemoteAddr, map[string]interface{}{ + if terr := models.NewAuditLogEntry(r, tx, user, models.DeleteRecoveryCodesAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, }); terr != nil { return terr @@ -461,8 +459,6 @@ func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) erro factor := getFactor(ctx) user := getUser(ctx) adminUser := getAdminUser(ctx) - instanceID := getInstanceID(ctx) - params := &adminUserUpdateFactorParams{} jsonDecoder := json.NewDecoder(r.Body) err := jsonDecoder.Decode(params) @@ -491,7 +487,7 @@ func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) erro } } - if terr := models.NewAuditLogEntry(tx, instanceID, adminUser, models.UpdateFactorAction, "", map[string]interface{}{ + if terr := models.NewAuditLogEntry(r, tx, adminUser, models.UpdateFactorAction, "", map[string]interface{}{ "user_id": user.ID, "factor_id": factor.ID, "factor_type": factor.FactorType, diff --git a/api/admin_test.go b/api/admin_test.go index 658430f80..71cfdc4b2 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -524,7 +524,7 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { // TestAdminUserDeleteRecoveryCodes tests API /admin/users//recovery_codes/ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { // TODO(Joel): Test case where factor is unverified - u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -555,7 +555,7 @@ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { // TestAdminUserDeleteFactor tests API /admin/users//factor// func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { // TODO(Joel): Test case where factor is unverified - u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -579,7 +579,7 @@ func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { // TestAdminUserGetFactor tests API /admin/user//factors/ func (ts *AdminTestSuite) TestAdminUserGetFactors() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -599,7 +599,7 @@ func (ts *AdminTestSuite) TestAdminUserGetFactors() { // TestAdminUserGetFactor tests API /admin/user//factors/ func (ts *AdminTestSuite) TestAdminUserGetFactor() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -618,7 +618,7 @@ func (ts *AdminTestSuite) TestAdminUserGetFactor() { } func (ts *AdminTestSuite) TestAdminUserUpdateFactor() { - u, err := models.NewUser(ts.instanceID, "123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) + u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") diff --git a/api/context.go b/api/context.go index 23b1e0001..5ce85e5f4 100644 --- a/api/context.go +++ b/api/context.go @@ -93,6 +93,8 @@ func getFactor(ctx context.Context) *models.Factor { return nil } return obj.(*models.Factor) +} + // withSession adds the session to the context. func withSession(ctx context.Context, s *models.Session) context.Context { return context.WithValue(ctx, sessionKey, s) diff --git a/api/mfa.go b/api/mfa.go index bcf30d4a9..909f9af42 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" @@ -60,7 +61,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { const imageSideLength = 300 ctx := r.Context() user := getUser(ctx) - instanceID := getInstanceID(ctx) params := &EnrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -98,7 +98,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if terr = tx.Create(factor); terr != nil { return terr } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.EnrollFactorAction, r.RemoteAddr, nil); terr != nil { + if terr := models.NewAuditLogEntry(r, tx, user, models.EnrollFactorAction, r.RemoteAddr, nil); terr != nil { return terr } return nil @@ -115,20 +115,22 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() - config := a.getConfig(ctx) + globalConfig, err := conf.LoadGlobal(configFile) + if err != nil { + return internalServerError("Error loading Config").WithInternalError(err) + } user := getUser(ctx) factor := getFactor(ctx) - instanceID := getInstanceID(ctx) challenge, terr := models.NewChallenge(factor) - if terr != nil { - return internalServerError("Database error creating challenge").WithInternalError(terr) + if err != nil { + return internalServerError("Database error creating challenge").WithInternalError(err) } terr = a.db.Transaction(func(tx *storage.Connection) error { if terr = tx.Create(challenge); terr != nil { return terr } - if terr := models.NewAuditLogEntry(tx, instanceID, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ + if terr := models.NewAuditLogEntry(r, tx, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ "factor_id": factor.ID, "factor_status": factor.Status, }); terr != nil { @@ -141,7 +143,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } creationTime := challenge.CreatedAt - expiryTime := creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)) + expiryTime := creationTime.Add(time.Second * time.Duration(globalConfig.MFA.ChallengeExpiryDuration)) return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, ExpiresAt: expiryTime.String(), @@ -151,10 +153,9 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { var err error ctx := r.Context() - config := a.getConfig(ctx) user := getUser(ctx) factor := getFactor(ctx) - instanceID := getInstanceID(ctx) + globalConfig, err := conf.LoadGlobal(configFile) params := &VerifyFactorParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -171,7 +172,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Database error finding Challenge").WithInternalError(err) } - hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration))) + hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(globalConfig.MFA.ChallengeExpiryDuration))) if hasExpired { err := a.db.Transaction(func(tx *storage.Connection) error { if terr := tx.Destroy(challenge); terr != nil { @@ -188,7 +189,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } err = a.db.Transaction(func(tx *storage.Connection) error { - if err = models.NewAuditLogEntry(tx, instanceID, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ + if err = models.NewAuditLogEntry(r, tx, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ "factor_id": factor.ID, "challenge_id": challenge.ID, }); err != nil { @@ -220,7 +221,6 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) factor := getFactor(ctx) - instanceID := getInstanceID(ctx) params := &UnenrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -244,7 +244,7 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { if err = tx.Destroy(factor); err != nil { return err } - if err = models.NewAuditLogEntry(tx, instanceID, user, models.UnenrollFactorAction, r.RemoteAddr, map[string]interface{}{ + if err = models.NewAuditLogEntry(r, tx, user, models.UnenrollFactorAction, r.RemoteAddr, map[string]interface{}{ "user_id": user.ID, "factor_id": factor.ID, }); err != nil { diff --git a/api/mfa_test.go b/api/mfa_test.go index 268ec30dc..5d2ad455c 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -10,7 +10,6 @@ import ( "testing" "time" - "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/models" "github.com/pquerna/otp/totp" @@ -20,18 +19,16 @@ import ( type MFATestSuite struct { suite.Suite - API *API - Config *conf.Configuration - instanceID uuid.UUID + API *API + Config *conf.GlobalConfiguration } func TestMFA(t *testing.T) { - api, config, instanceID, err := setupAPIForTestForInstance() + api, config, err := setupAPIForTest() require.NoError(t, err) ts := &MFATestSuite{ - API: api, - Config: config, - instanceID: instanceID, + API: api, + Config: config, } defer api.db.Close() @@ -41,7 +38,7 @@ func TestMFA(t *testing.T) { func (ts *MFATestSuite) SetupTest() { models.TruncateAll(ts.API.db) // Create user - u, err := models.NewUser(ts.instanceID, "123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) + u, err := models.NewUser("123456789", "test@example.com", "password", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") // Create Factor @@ -91,10 +88,10 @@ func (ts *MFATestSuite) TestEnrollFactor() { ts.Run(c.desc, func() { var buffer bytes.Buffer require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]string{"friendly_name": c.FriendlyName, "factor_type": c.FactorType, "issuer": c.Issuer})) - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + user, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) - token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(user, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -117,13 +114,13 @@ func (ts *MFATestSuite) TestEnrollFactor() { } func (ts *MFATestSuite) TestChallengeFactor() { - u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err) f, err := models.FindFactorByFactorID(ts.API.db, "testFactorID") require.NoError(ts.T(), err) - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err, "Error generating access token") var buffer bytes.Buffer @@ -164,7 +161,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { } for _, v := range cases { ts.Run(v.desc, func() { - u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) emailValue, err := u.Email.Value() require.NoError(ts.T(), err) testEmail := emailValue.(string) @@ -194,7 +191,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { } // Verify the user - user, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, testEmail, ts.Config.JWT.Aud) + user, err := models.FindUserByEmailAndAudience(ts.API.db, testEmail, ts.Config.JWT.Aud) ts.Require().NoError(err) code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) if !v.validCode { @@ -208,7 +205,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { "code": code, })) - token, err := generateAccessToken(user, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(user, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -233,7 +230,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { func (ts *MFATestSuite) TestUnenrollFactor() { // TODO(Joel): Test case where factor is unverified - u, err := models.FindUserByEmailAndAudience(ts.API.db, ts.instanceID, "test@example.com", ts.Config.JWT.Aud) + u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) emailValue, err := u.Email.Value() require.NoError(ts.T(), err) testEmail := emailValue.(string) @@ -253,7 +250,7 @@ func (ts *MFATestSuite) TestUnenrollFactor() { var buffer bytes.Buffer - token, err := generateAccessToken(u, time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) diff --git a/api/signup_test.go b/api/signup_test.go index 3d0879805..6e43cbe57 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -110,7 +110,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { u, ok := data["user"].(map[string]interface{}) require.True(ok) - assert.Len(u, 10) + assert.Len(u, 11) assert.Equal("authenticated", u["aud"]) assert.Equal("authenticated", u["role"]) assert.Equal("test@example.com", u["email"]) diff --git a/go.mod b/go.mod index ebec8a913..5b7913683 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/netlify/mailme v1.1.1 github.com/opentracing/opentracing-go v1.1.0 github.com/pkg/errors v0.9.1 - github.com/pquerna/otp v1.3.0 // indirect + github.com/pquerna/otp v1.3.0 github.com/rs/cors v1.6.0 github.com/sebest/xff v0.0.0-20160910043805-6c115e0ffa35 github.com/sethvargo/go-password v0.2.0 @@ -36,9 +36,12 @@ require ( gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) +require github.com/gobuffalo/nulls v0.4.0 + require ( github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.10.0 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -48,7 +51,6 @@ require ( github.com/gobuffalo/flect v0.2.2 // indirect github.com/gobuffalo/github_flavored_markdown v1.1.0 // indirect github.com/gobuffalo/helpers v0.6.1 // indirect - github.com/gobuffalo/nulls v0.4.0 // indirect github.com/gobuffalo/packd v1.0.0 // indirect github.com/gobuffalo/packr/v2 v2.8.1 // indirect github.com/gobuffalo/plush/v4 v4.1.0 // indirect diff --git a/models/challenge_test.go b/models/challenge_test.go index 97ed3d2e5..1f16c94a2 100644 --- a/models/challenge_test.go +++ b/models/challenge_test.go @@ -1,7 +1,6 @@ package models import ( - "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" @@ -32,7 +31,7 @@ func (ts *ChallengeTestSuite) SetupTest() { } func (ts *FactorTestSuite) TestFindChallengesByFactorID() { - u, err := NewUser(uuid.Nil, "", "genericemail@gmail.com", "secret", "test", nil) + u, err := NewUser("", "genericemail@gmail.com", "secret", "test", nil) require.NoError(ts.T(), err) err = ts.db.Create(u) require.NoError(ts.T(), err) diff --git a/models/factor_test.go b/models/factor_test.go index c660ffab3..a319e9cee 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -1,7 +1,6 @@ package models import ( - "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/storage" "github.com/netlify/gotrue/storage/test" @@ -58,7 +57,7 @@ func (ts *FactorTestSuite) TestFindFactorByFactorID() { } func (ts *FactorTestSuite) createFactor() *Factor { - user, err := NewUser(uuid.Nil, "", "agenericemail@gmail.com", "secret", "test", nil) + user, err := NewUser("", "agenericemail@gmail.com", "secret", "test", nil) require.NoError(ts.T(), err) err = ts.db.Create(user) @@ -74,7 +73,7 @@ func (ts *FactorTestSuite) createFactor() *Factor { } func (ts *FactorTestSuite) TestUpdateStatus() { newFactorStatus := FactorVerifiedState - u, err := NewUser(uuid.Nil, "", "", "", "", nil) + u, err := NewUser("", "", "", "", nil) require.NoError(ts.T(), err) f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorDisabledState, "") @@ -85,7 +84,7 @@ func (ts *FactorTestSuite) TestUpdateStatus() { func (ts *FactorTestSuite) TestUpdateFriendlyName() { newSimpleName := "newFactorName" - u, err := NewUser(uuid.Nil, "", "", "", "", nil) + u, err := NewUser("", "", "", "", nil) require.NoError(ts.T(), err) f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorDisabledState, "") diff --git a/models/instance.go b/models/instance.go index 4cd2ba7f0..3d781ba99 100644 --- a/models/instance.go +++ b/models/instance.go @@ -18,7 +18,7 @@ type Instance struct { // Netlify UUID UUID uuid.UUID `json:"uuid,omitempty" db:"uuid"` - BaseConfig *conf.Configuration `json:"config" db:"raw_base_config"` + BaseConfig *conf.GlobalConfiguration `json:"config" db:"raw_base_config"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` @@ -30,12 +30,12 @@ func (Instance) TableName() string { } // Config loads the the base configuration values with defaults. -func (i *Instance) Config() (*conf.Configuration, error) { +func (i *Instance) Config() (*conf.GlobalConfiguration, error) { if i.BaseConfig == nil { return nil, errors.New("no configuration data available") } - baseConf := &conf.Configuration{} + baseConf := &conf.GlobalConfiguration{} *baseConf = *i.BaseConfig baseConf.ApplyDefaults() @@ -43,7 +43,7 @@ func (i *Instance) Config() (*conf.Configuration, error) { } // UpdateConfig updates the base config -func (i *Instance) UpdateConfig(tx *storage.Connection, config *conf.Configuration) error { +func (i *Instance) UpdateConfig(tx *storage.Connection, config *conf.GlobalConfiguration) error { i.BaseConfig = config return tx.UpdateOnly(i, "raw_base_config") } diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 82993f346..98584526c 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -2,7 +2,6 @@ package models import ( "fmt" - "github.com/gofrs/uuid" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" @@ -39,7 +38,7 @@ func TestRecoveryCode(t *testing.T) { func (ts *RecoveryCodeTestSuite) TestFindValidRecoveryCodesByUser() { var expectedRecoveryCodes []string - user, err := NewUser(uuid.Nil, "", "", "", "", nil) + user, err := NewUser("", "", "", "", nil) err = ts.db.Create(user) require.NoError(ts.T(), err) for i := 0; i <= NumRecoveryCodes; i++ { diff --git a/models/user.go b/models/user.go index 358f0c520..84f303720 100644 --- a/models/user.go +++ b/models/user.go @@ -56,7 +56,7 @@ type User struct { AppMetaData JSONMap `json:"app_metadata" db:"raw_app_meta_data"` UserMetaData JSONMap `json:"user_metadata" db:"raw_user_meta_data"` - Factors []Factor `json:"factors" has_many:"factors"` + Factors []Factor `json:"factors" has_many:"factors"` Identities []Identity `json:"identities" has_many:"identities"` CreatedAt time.Time `json:"created_at" db:"created_at"` From 30cc63c46271466ace8a8da5db630383692b6508 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Mon, 29 Aug 2022 15:22:52 +0530 Subject: [PATCH 098/180] Add MFA Sessions Fields (#643) * feat: add session fields * fix: handle terr * fix: change column name Co-authored-by: joel@joellee.org --- api/mfa.go | 12 +++++++++--- conf/configuration.go | 3 +-- migrations/20220811173540_add_sessions_table.up.sql | 3 +++ models/challenge.go | 3 +-- models/sessions.go | 2 ++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 909f9af42..4119280c5 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -103,6 +103,9 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } return nil }) + if terr != nil { + return terr + } return sendJSON(w, http.StatusOK, &EnrollFactorResponse{ ID: factor.ID, Type: factor.FactorType, @@ -171,6 +174,10 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return internalServerError("Database error finding Challenge").WithInternalError(err) } + valid := totp.Validate(params.Code, factor.SecretKey) + if !valid { + return unauthorizedError("Invalid TOTP code entered") + } hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(globalConfig.MFA.ChallengeExpiryDuration))) if hasExpired { @@ -205,9 +212,8 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return nil }) - valid := totp.Validate(params.Code, factor.SecretKey) - if !valid { - return unauthorizedError("Invalid TOTP code entered") + if err != nil { + return err } return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ diff --git a/conf/configuration.go b/conf/configuration.go index 259ccd4a7..821aad986 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -49,8 +49,7 @@ type JWTConfiguration struct { // MFAConfiguration holds all the MFA related Configuration type MFAConfiguration struct { - Enabled bool `json:"mfa_enabled" default:"false"` - ChallengeExpiryDuration float64 `json:"challenge_expiry_duration" default:"300"` + ChallengeExpiryDuration float64 `json:"challenge_expiry_duration" default:"300" split_words:"true"` } // GlobalConfiguration holds all the configuration that applies to all instances. diff --git a/migrations/20220811173540_add_sessions_table.up.sql b/migrations/20220811173540_add_sessions_table.up.sql index 7c4badcba..816ba43c6 100644 --- a/migrations/20220811173540_add_sessions_table.up.sql +++ b/migrations/20220811173540_add_sessions_table.up.sql @@ -4,6 +4,9 @@ create table if not exists auth.sessions ( user_id uuid not null, created_at timestamptz null, updated_at timestamptz null, + factor_id text null, + amr_claims text [] null, + constraint sessions_pkey primary key (id), constraint sessions_user_id_fkey foreign key (user_id) references auth.users(id) on delete cascade ); diff --git a/models/challenge.go b/models/challenge.go index 7d015a7a9..231087153 100644 --- a/models/challenge.go +++ b/models/challenge.go @@ -54,7 +54,7 @@ func FindChallengesByFactorID(tx *storage.Connection, factorID string) ([]*Chall func (f *Challenge) Verify(tx *storage.Connection) error { now := time.Now() f.VerifiedAt = &now - return tx.UpdateOnly(f, "verifiedAt") + return tx.UpdateOnly(f, "verified_at") } func findChallenge(tx *storage.Connection, query string, args ...interface{}) (*Challenge, error) { @@ -65,6 +65,5 @@ func findChallenge(tx *storage.Connection, query string, args ...interface{}) (* } return nil, errors.Wrap(err, "error finding challenge") } - return obj, nil } diff --git a/models/sessions.go b/models/sessions.go index 331d9fe5a..cc76c5568 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -15,6 +15,8 @@ type Session struct { UserID uuid.UUID `json:"user_id" db:"user_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + FactorID string `json:"factor_id" db:"factor_id"` + AMRClaims []string `json:"amr_claims" db:"amr_claims"` } func (Session) TableName() string { From b36b33772a47cfd6f054702806f822e0cb40a689 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Mon, 29 Aug 2022 17:55:01 +0530 Subject: [PATCH 099/180] refactor: move constants into models (#646) * test: check for unverified case * refactor: move constants into models layer * chore: test semantic release * chore: update comment Co-authored-by: joel@joellee.org --- api/mfa.go | 13 +++---- api/mfa_test.go | 94 ++++++++++++++++++++++++++++++------------------ models/errors.go | 2 ++ models/factor.go | 5 +++ 4 files changed, 74 insertions(+), 40 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 4119280c5..d9362d005 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -57,7 +57,7 @@ type UnenrollFactorParams struct { } func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { - const factorPrefix = "factor" + // TODO(joel): Gate the endpoint with a config var such that only one factor can be enrolled const imageSideLength = 300 ctx := r.Context() user := getUser(ctx) @@ -68,7 +68,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if err != nil { return badRequestError(err.Error()) } - if (params.FactorType != "totp") && (params.FactorType != "webauthn") { + factorType := params.FactorType + if (factorType != models.TOTP) && (factorType != models.Webauthn) { return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") } if params.Issuer == "" { @@ -89,8 +90,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Error generating QR Code image").WithInternalError(err) } qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) - factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, models.FactorDisabledState, key.Secret()) + factorID := fmt.Sprintf("%s_%s", models.FactorPrefix, crypto.SecureToken()) + factor, terr := models.NewFactor(user, params.FriendlyName, factorID, factorType, models.FactorDisabledState, key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -202,11 +203,11 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { }); err != nil { return err } - if err = challenge.Verify(a.db); err != nil { + if err = challenge.Verify(tx); err != nil { return err } if factor.Status != models.FactorVerifiedState { - if err = factor.UpdateStatus(a.db, models.FactorVerifiedState); err != nil { + if err = factor.UpdateStatus(tx, models.FactorVerifiedState); err != nil { return err } } diff --git a/api/mfa_test.go b/api/mfa_test.go index 5d2ad455c..0c3512fcb 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -48,6 +48,7 @@ func (ts *MFATestSuite) SetupTest() { } func (ts *MFATestSuite) TestEnrollFactor() { + // TODO(Joel): Check that only one factor can be enrolled var cases = []struct { desc string FriendlyName string @@ -229,43 +230,68 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { } func (ts *MFATestSuite) TestUnenrollFactor() { - // TODO(Joel): Test case where factor is unverified - u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) - emailValue, err := u.Email.Value() - require.NoError(ts.T(), err) - testEmail := emailValue.(string) - testDomain := strings.Split(testEmail, "@")[1] - // set factor secret - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: testDomain, - AccountName: testEmail, - }) - sharedSecret := key.Secret() - factors, err := models.FindFactorsByUser(ts.API.db, u) - f := factors[0] - f.SecretKey = sharedSecret - err = f.UpdateStatus(ts.API.db, models.FactorVerifiedState) - require.NoError(ts.T(), err) - require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") + cases := []struct { + desc string + IsFactorVerified bool + ExpectedHTTPCode int + }{ + { + "Unverified Factor", + false, + http.StatusForbidden, + }, + { + "Verified Factor", + true, + http.StatusOK, + }, + } + for _, v := range cases { - var buffer bytes.Buffer + ts.Run(v.desc, func() { + u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) + emailValue, err := u.Email.Value() + require.NoError(ts.T(), err) + testEmail := emailValue.(string) + testDomain := strings.Split(testEmail, "@")[1] + // Set factor secret + key, err := totp.Generate(totp.GenerateOpts{ + Issuer: testDomain, + AccountName: testEmail, + }) + sharedSecret := key.Secret() + factors, err := models.FindFactorsByUser(ts.API.db, u) + f := factors[0] + f.SecretKey = sharedSecret + if v.IsFactorVerified { + err = f.UpdateStatus(ts.API.db, models.FactorVerifiedState) + require.NoError(ts.T(), err) + } - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) - require.NoError(ts.T(), err) + require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") - code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) - require.NoError(ts.T(), err) - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "factor_id": f.ID, - "code": code, - })) + var buffer bytes.Buffer - w := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/user/%s/factor/%s/", u.ID, f.ID), &buffer) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), http.StatusOK, w.Code) - _, err = models.FindFactorByFactorID(ts.API.db, f.ID) - require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + + code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) + require.NoError(ts.T(), err) + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "factor_id": f.ID, + "code": code, + })) + + w := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/user/%s/factor/%s/", u.ID, f.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), v.ExpectedHTTPCode, w.Code) + if v.IsFactorVerified { + _, err = models.FindFactorByFactorID(ts.API.db, f.ID) + require.EqualError(ts.T(), err, models.FactorNotFoundError{}.Error()) + } + }) + } } diff --git a/models/errors.go b/models/errors.go index d21460e45..ce7b6435e 100644 --- a/models/errors.go +++ b/models/errors.go @@ -19,6 +19,8 @@ func IsNotFoundError(err error) bool { return true case ChallengeNotFoundError: return true + case FactorNotFoundError: + return true } return false } diff --git a/models/factor.go b/models/factor.go index 6a0abc1bc..4c048419a 100644 --- a/models/factor.go +++ b/models/factor.go @@ -8,10 +8,15 @@ import ( "time" ) +const FactorPrefix = "factor" + const FactorDisabledState = "disabled" const FactorUnverifiedState = "unverified" const FactorVerifiedState = "verified" +const TOTP = "totp" +const Webauthn = "webauthn" + type Factor struct { ID string `json:"id" db:"id"` User User `belongs_to:"user"` From 9c009e4b7a2062aad0f99f531ccfba430170f97d Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Mon, 29 Aug 2022 19:55:37 +0530 Subject: [PATCH 100/180] feat: Convert QR to SVG (#624) * initial commit * chore:add qrcodesize param * refactor: strip our QRCodeSize, undo session changes Co-authored-by: joel@joellee.org --- api/mfa.go | 28 +++++++++++++++------------- go.mod | 16 +++++++++++----- go.sum | 29 +++++++++++++++++++++++++++++ models/mfa_constants.go | 1 + models/sessions.go | 3 +-- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index d9362d005..f3c8c2448 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -2,15 +2,16 @@ package api import ( "bytes" - "encoding/base64" "encoding/json" "fmt" + "github.com/aaronarduino/goqrsvg" + "github.com/ajstarks/svgo" + "github.com/boombuler/barcode/qr" "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" "github.com/pquerna/otp/totp" - "image/png" "net/http" "time" ) @@ -57,8 +58,7 @@ type UnenrollFactorParams struct { } func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { - // TODO(joel): Gate the endpoint with a config var such that only one factor can be enrolled - const imageSideLength = 300 + const factorPrefix = "factor" ctx := r.Context() user := getUser(ctx) @@ -84,14 +84,15 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { return internalServerError("Error generating QR Code secret key").WithInternalError(err) } var buf bytes.Buffer - img, err := key.Image(imageSideLength, imageSideLength) - png.Encode(&buf, img) - if err != nil { - return internalServerError("Error generating QR Code image").WithInternalError(err) - } - qrAsBase64 := base64.StdEncoding.EncodeToString(buf.Bytes()) - factorID := fmt.Sprintf("%s_%s", models.FactorPrefix, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.FriendlyName, factorID, factorType, models.FactorDisabledState, key.Secret()) + s := svg.New(&buf) + qrCode, _ := qr.Encode(key.String(), qr.M, qr.Auto) + qs := goqrsvg.NewQrSVG(qrCode, models.DefaultQRSize) + qs.StartQrSVG(s) + qs.WriteQrSVG(s) + s.End() + + factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) + factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, models.FactorDisabledState, key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -111,7 +112,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { ID: factor.ID, Type: factor.FactorType, TOTP: TOTPObject{ - QRCode: fmt.Sprintf("data:img/png;base64,%v", qrAsBase64), + // See: https://css-tricks.com/probably-dont-base64-svg/ + QRCode: fmt.Sprintf("data:img/svg+xml;utf-8,%v", &buf), Secret: factor.SecretKey, URI: key.URL(), }, diff --git a/go.mod b/go.mod index 5b7913683..681bef86d 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,25 @@ module github.com/netlify/gotrue require ( + cloud.google.com/go v0.67.0 // indirect + github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240 + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40 + github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62 + github.com/beevik/etree v1.1.0 + github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc github.com/coreos/go-oidc/v3 v3.0.0 github.com/didip/tollbooth/v5 v5.1.1 github.com/go-chi/chi v4.0.2+incompatible github.com/gobuffalo/pop/v5 v5.3.3 + github.com/gobuffalo/validate/v3 v3.3.0 // indirect github.com/gobwas/glob v0.2.3 github.com/gofrs/uuid v4.0.0+incompatible github.com/golang-jwt/jwt v3.2.1+incompatible github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.1.1 + github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639 github.com/jackc/pgconn v1.8.0 github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 github.com/jackc/pgproto3/v2 v2.0.7 // indirect @@ -18,6 +27,8 @@ require ( github.com/joho/godotenv v1.3.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/lestrrat-go/jwx v0.9.0 + github.com/lib/pq v1.9.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.19 // indirect github.com/mitchellh/mapstructure v1.1.2 github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 github.com/netlify/mailme v1.1.1 @@ -39,9 +50,7 @@ require ( require github.com/gobuffalo/nulls v0.4.0 require ( - github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.10.0 // indirect github.com/fatih/structs v1.1.0 // indirect @@ -55,7 +64,6 @@ require ( github.com/gobuffalo/packr/v2 v2.8.1 // indirect github.com/gobuffalo/plush/v4 v4.1.0 // indirect github.com/gobuffalo/tags/v3 v3.1.0 // indirect - github.com/gobuffalo/validate/v3 v3.3.0 // indirect github.com/golang/protobuf v1.4.2 // indirect github.com/google/go-cmp v0.5.2 // indirect github.com/gorilla/context v1.1.1 // indirect @@ -69,12 +77,10 @@ require ( github.com/jackc/pgx/v4 v4.10.1 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pretty v0.3.0 // indirect - github.com/lib/pq v1.9.0 // indirect github.com/luna-duclos/instrumentedsql v1.1.3 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect - github.com/microcosm-cc/bluemonday v1.0.16 // indirect github.com/netlify/netlify-commons v0.32.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/philhofer/fwd v1.0.0 // indirect diff --git a/go.sum b/go.sum index eb09731fc..ba70af122 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,7 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -35,10 +36,17 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20170623214735-571947b0f240/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo= github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40 h1:uz4N2yHL4MF8vZX+36n+tcxeUf8D/gL4aJkyouhDw4A= +github.com/aaronarduino/goqrsvg v0.0.0-20220419053939-17e843f1dd40/go.mod h1:dytw+5qs+pdi61fO/S4OmXR7AuEq/HvNCuG03KxQHT4= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -50,6 +58,7 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62 h1:vMqcPzLT1/mbYew0gM6EJy4/sCNy9lY9rmlFO+pPwhY= github.com/badoux/checkmail v0.0.0-20170203135005-d0a759655d62/go.mod h1:r5ZalvRl3tXevRNJkwIB6DC4DD3DMjIlY9NEU1XGoaQ= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= @@ -220,6 +229,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -263,6 +273,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/raft v1.1.0/go.mod h1:4Ak7FSPnuvmb0GV6vgIAJ4vYT4bek9bb6Q+7HVbyzqM= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.0.0-20160216103600-3e95a51e0639/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= @@ -400,6 +411,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/microcosm-cc/bluemonday v1.0.16 h1:kHmAq2t7WPWLjiGvzKa5o3HzSfahUKiOq7fAPUiMNIc= github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/microcosm-cc/bluemonday v1.0.19 h1:OI7hoF5FY4pFz2VA//RN8TfM0YJ2dJcl4P4APrCWy6c= +github.com/microcosm-cc/bluemonday v1.0.19/go.mod h1:QNzV2UbLK2/53oIIwTOyLUSABMkjZ4tqiyC1g/DyqxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -545,6 +558,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -644,6 +658,8 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba h1:6u6sik+bn/y7vILcYkK3iwTBWN7WtBvB0+SZswQnbf8= golang.org/x/net v0.0.0-20220121210141-e204ce36a2ba/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -662,6 +678,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -704,7 +721,10 @@ golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -775,6 +795,9 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -798,6 +821,7 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -835,6 +859,8 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -848,6 +874,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -896,6 +924,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/models/mfa_constants.go b/models/mfa_constants.go index 5df611b01..abc2f9b6b 100644 --- a/models/mfa_constants.go +++ b/models/mfa_constants.go @@ -2,3 +2,4 @@ package models const NumRecoveryCodes = 8 const RecoveryCodeLength = 8 +const DefaultQRSize = 3 diff --git a/models/sessions.go b/models/sessions.go index cc76c5568..ac413cf9e 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -15,8 +15,6 @@ type Session struct { UserID uuid.UUID `json:"user_id" db:"user_id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` - FactorID string `json:"factor_id" db:"factor_id"` - AMRClaims []string `json:"amr_claims" db:"amr_claims"` } func (Session) TableName() string { @@ -29,6 +27,7 @@ func NewSession(user *User) (*Session, error) { if err != nil { return nil, errors.Wrap(err, "Error generating unique session id") } + session := &Session{ ID: id, UserID: user.ID, From 5738884e18a4f36d89cdf96d3b28aa5fa4e17615 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 29 Aug 2022 20:20:47 +0530 Subject: [PATCH 101/180] fix: change method of reading from config --- api/mfa.go | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index f3c8c2448..9d91c3978 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -7,7 +7,6 @@ import ( "github.com/aaronarduino/goqrsvg" "github.com/ajstarks/svgo" "github.com/boombuler/barcode/qr" - "github.com/netlify/gotrue/conf" "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" @@ -121,19 +120,17 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { } func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() - globalConfig, err := conf.LoadGlobal(configFile) - if err != nil { - return internalServerError("Error loading Config").WithInternalError(err) - } + config := a.config + user := getUser(ctx) factor := getFactor(ctx) - challenge, terr := models.NewChallenge(factor) + challenge, err := models.NewChallenge(factor) if err != nil { return internalServerError("Database error creating challenge").WithInternalError(err) } - terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr = tx.Create(challenge); terr != nil { + terr := a.db.Transaction(func(tx *storage.Connection) error { + if terr := tx.Create(challenge); terr != nil { return terr } if terr := models.NewAuditLogEntry(r, tx, user, models.CreateChallengeAction, r.RemoteAddr, map[string]interface{}{ @@ -149,7 +146,7 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { } creationTime := challenge.CreatedAt - expiryTime := creationTime.Add(time.Second * time.Duration(globalConfig.MFA.ChallengeExpiryDuration)) + expiryTime := creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)) return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, ExpiresAt: expiryTime.String(), @@ -161,7 +158,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) factor := getFactor(ctx) - globalConfig, err := conf.LoadGlobal(configFile) + config := a.config params := &VerifyFactorParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -182,7 +179,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return unauthorizedError("Invalid TOTP code entered") } - hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(globalConfig.MFA.ChallengeExpiryDuration))) + hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration))) if hasExpired { err := a.db.Transaction(func(tx *storage.Connection) error { if terr := tx.Destroy(challenge); terr != nil { From 3df30939ec85bed9b7b89b99673d7ae0637a4c68 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 29 Aug 2022 20:24:28 +0530 Subject: [PATCH 102/180] refactor: remove unused var --- models/instance.go | 1 - 1 file changed, 1 deletion(-) diff --git a/models/instance.go b/models/instance.go index 3d781ba99..231cbd196 100644 --- a/models/instance.go +++ b/models/instance.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" ) -const baseConfigKey = "" type Instance struct { ID uuid.UUID `json:"id" db:"id"` From 65d425576e0d1a8088a26d1d4a5508e6aca4db01 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 29 Aug 2022 20:49:07 +0530 Subject: [PATCH 103/180] fix: patch gosec errors --- api/mfa.go | 6 +++++- models/instance.go | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 9d91c3978..c7168b370 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -87,7 +87,11 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { qrCode, _ := qr.Encode(key.String(), qr.M, qr.Auto) qs := goqrsvg.NewQrSVG(qrCode, models.DefaultQRSize) qs.StartQrSVG(s) - qs.WriteQrSVG(s) + err = qs.WriteQrSVG(s) + if err != nil { + return internalServerError("Error writing to QR Code").WithInternalError(err) + } + s.End() factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) diff --git a/models/instance.go b/models/instance.go index 231cbd196..ef85a8b1e 100644 --- a/models/instance.go +++ b/models/instance.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" ) - type Instance struct { ID uuid.UUID `json:"id" db:"id"` // Netlify UUID From d0a5966217e7f539f48fc6ef37f165553447ddd3 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Mon, 29 Aug 2022 21:52:52 +0530 Subject: [PATCH 104/180] feat: cherry-pick step up changes onto separate branch (#652) * refactor: cherry pick onto separate branch * fix: update recovery code files * feat: cherry-pick partial changes from stepup login branch * Update models/recovery_code.go Co-authored-by: joel@joellee.org --- api/admin.go | 2 +- api/admin_test.go | 7 ++- api/mfa.go | 116 ++++++++++++++++++++++++++++++++++- api/mfa_test.go | 4 +- models/audit_log_entry.go | 2 + models/challenge_test.go | 2 +- models/factor.go | 1 - models/factor_test.go | 4 +- models/recovery_code.go | 48 +++++++++++++++ models/recovery_code_test.go | 14 +++++ 10 files changed, 189 insertions(+), 11 deletions(-) diff --git a/api/admin.go b/api/admin.go index 18eb9418e..7c7b5d3da 100644 --- a/api/admin.go +++ b/api/admin.go @@ -501,5 +501,5 @@ func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) erro } func isValidFactorStatus(factorStatus string) bool { - return factorStatus == models.FactorVerifiedState || factorStatus == models.FactorUnverifiedState || factorStatus == models.FactorDisabledState + return factorStatus == models.FactorVerifiedState || factorStatus == models.FactorUnverifiedState } diff --git a/api/admin_test.go b/api/admin_test.go index 71cfdc4b2..10bf6237e 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -560,6 +560,7 @@ func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorVerifiedState, "secretkey") + require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") @@ -583,7 +584,7 @@ func (ts *AdminTestSuite) TestAdminUserGetFactors() { require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorUnverifiedState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") @@ -603,7 +604,7 @@ func (ts *AdminTestSuite) TestAdminUserGetFactor() { require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorUnverifiedState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") @@ -622,7 +623,7 @@ func (ts *AdminTestSuite) TestAdminUserUpdateFactor() { require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorUnverifiedState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") diff --git a/api/mfa.go b/api/mfa.go index c7168b370..1b9a396cd 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -8,6 +8,7 @@ import ( "github.com/ajstarks/svgo" "github.com/boombuler/barcode/qr" "github.com/netlify/gotrue/crypto" + "github.com/netlify/gotrue/metering" "github.com/netlify/gotrue/models" "github.com/netlify/gotrue/storage" "github.com/pquerna/otp/totp" @@ -56,6 +57,12 @@ type UnenrollFactorParams struct { Code string `json:"code"` } +type StepUpLoginParams struct { + ChallengeID string `json:"challenge_id"` + Code string `json:"code"` + RecoveryCode string `json:"recovery_code"` +} + func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { const factorPrefix = "factor" ctx := r.Context() @@ -95,7 +102,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { s.End() factorID := fmt.Sprintf("%s_%s", factorPrefix, crypto.SecureToken()) - factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, models.FactorDisabledState, key.Secret()) + factor, terr := models.NewFactor(user, params.FriendlyName, factorID, params.FactorType, models.FactorUnverifiedState, key.Secret()) if terr != nil { return internalServerError("Database error creating factor").WithInternalError(err) } @@ -157,6 +164,113 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { }) } +// TODO(Joel): Move over other supporting changes from other branch. Don't use until properly tested. +func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { + ctx := r.Context() + config := a.config + user := getUser(ctx) + factor := getFactor(ctx) + + if factor.Status != models.FactorVerifiedState { + return unprocessableEntityError("Please attempt a login with a verified factor") + } + + params := &StepUpLoginParams{} + jsonDecoder := json.NewDecoder(r.Body) + err := jsonDecoder.Decode(params) + if err != nil { + return badRequestError("Please check the params passed into StepupLogin: %v", err) + } + if params.Code != "" && params.RecoveryCode != "" { + return unprocessableEntityError("Please attempt a login with only one of Code or Recovery Code'") + } + + if params.Code != "" { + // TODO(suggest): Either reorganize to token grant style case statement with types OR dump this into models + challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) + if err != nil { + if models.IsNotFoundError(err) { + return notFoundError(err.Error()) + } + return internalServerError("Database error finding Challenge").WithInternalError(err) + } + hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration))) + if hasExpired { + err := a.db.Transaction(func(tx *storage.Connection) error { + if terr := tx.Destroy(challenge); terr != nil { + return internalServerError("Database error deleting challenge").WithInternalError(terr) + } + + return nil + }) + if err != nil { + return err + } + + return expiredChallengeError("%v has expired, please verify against another challenge or create a new challenge.", challenge.ID) + } + valid := totp.Validate(params.Code, factor.SecretKey) + if !valid { + return unauthorizedError("Invalid code entered") + } + } else if params.RecoveryCode != "" { + // TODO(suggest): Shorten session duration for sessions arising from recovery code + err := a.db.Transaction(func(tx *storage.Connection) error { + rc, terr := models.IsRecoveryCodeValid(tx, user, params.RecoveryCode) + if terr != nil { + return terr + } + if rc.RecoveryCode == params.RecoveryCode { + terr = rc.Consume(tx) + if terr != nil { + return terr + } + } else { + return unauthorizedError("Invalid code entered") + } + + return nil + + }) + if err != nil { + return err + } + return unauthorizedError("Invalid code entered") + } + var token *AccessTokenResponse + + err = a.db.Transaction(func(tx *storage.Connection) error { + var terr error + if terr = models.NewAuditLogEntry(r, tx, user, models.MFALoginAction, "", nil); terr != nil { + return terr + } + // TODO(joel): Reinstate the TOTP claim when we add the claims logic to all endpoints + token, terr = a.issueRefreshToken(ctx, tx, user) + if terr != nil { + return terr + } + + if terr = a.setCookieTokens(config, token, false, w); terr != nil { + return internalServerError("Failed to set JWT cookie. %s", terr) + } + return nil + }) + if err != nil { + return err + } + metering.RecordLogin("token", user.ID) + // if user.IsFirstMFALogin(){ + // // Wrap this in a transaction + // recoveryCodes, err := models.GenerateRecoveryCodesBatch() + // return sendJSON(w, http.StatusOK, StepUpLoginResponse{ + // token: token + // recovery_code: recoveryCodes + // }) + // } + + return sendJSON(w, http.StatusOK, token) +} + func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { var err error ctx := r.Context() diff --git a/api/mfa_test.go b/api/mfa_test.go index 0c3512fcb..dfbbe9981 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -42,7 +42,7 @@ func (ts *MFATestSuite) SetupTest() { require.NoError(ts.T(), err, "Error creating test user model") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") // Create Factor - f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorDisabledState, "secretkey") + f, err := models.NewFactor(u, "testSimpleName", "testFactorID", "totp", models.FactorUnverifiedState, "secretkey") require.NoError(ts.T(), err, "Error creating test factor model") require.NoError(ts.T(), ts.API.db.Create(f), "Error saving new test factor") } @@ -105,7 +105,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { factors, err := models.FindFactorsByUser(ts.API.db, user) ts.Require().NoError(err) latestFactor := factors[len(factors)-1] - require.Equal(ts.T(), models.FactorDisabledState, latestFactor.Status) + require.Equal(ts.T(), models.FactorUnverifiedState, latestFactor.Status) if c.FriendlyName != "" && c.ExpectedCode == http.StatusOK { require.Equal(ts.T(), c.FriendlyName, latestFactor.FriendlyName) } diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 0de3007b5..3c6ac27c0 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -38,6 +38,7 @@ const ( DeleteFactorAction AuditAction = "factor_deleted" DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" UpdateFactorAction AuditAction = "factor_updated" + MFALoginAction AuditAction = "mfa_login" account auditLogType = "account" team auditLogType = "team" @@ -67,6 +68,7 @@ var ActionLogTypeMap = map[AuditAction]auditLogType{ VerifyFactorAction: factor, DeleteFactorAction: factor, UpdateFactorAction: factor, + MFALoginAction: factor, DeleteRecoveryCodesAction: recoveryCodes, } diff --git a/models/challenge_test.go b/models/challenge_test.go index 1f16c94a2..26d64c585 100644 --- a/models/challenge_test.go +++ b/models/challenge_test.go @@ -35,7 +35,7 @@ func (ts *FactorTestSuite) TestFindChallengesByFactorID() { require.NoError(ts.T(), err) err = ts.db.Create(u) require.NoError(ts.T(), err) - f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "totp", FactorDisabledState, "topsecret") + f, err := NewFactor(u, "asimplename", "factor-which-shall-not-be-named", "totp", FactorUnverifiedState, "topsecret") require.NoError(ts.T(), err) err = ts.db.Create(f) require.NoError(ts.T(), err) diff --git a/models/factor.go b/models/factor.go index 4c048419a..fe32c782d 100644 --- a/models/factor.go +++ b/models/factor.go @@ -10,7 +10,6 @@ import ( const FactorPrefix = "factor" -const FactorDisabledState = "disabled" const FactorUnverifiedState = "unverified" const FactorVerifiedState = "verified" diff --git a/models/factor_test.go b/models/factor_test.go index a319e9cee..6357c9769 100644 --- a/models/factor_test.go +++ b/models/factor_test.go @@ -76,7 +76,7 @@ func (ts *FactorTestSuite) TestUpdateStatus() { u, err := NewUser("", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorDisabledState, "") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorUnverifiedState, "") require.NoError(ts.T(), err) require.NoError(ts.T(), f.UpdateStatus(ts.db, newFactorStatus)) require.Equal(ts.T(), newFactorStatus, f.Status) @@ -87,7 +87,7 @@ func (ts *FactorTestSuite) TestUpdateFriendlyName() { u, err := NewUser("", "", "", "", nil) require.NoError(ts.T(), err) - f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorDisabledState, "") + f, err := NewFactor(u, "A1B2C3", "testfactor-id", "some-secret", FactorUnverifiedState, "") require.NoError(ts.T(), err) require.NoError(ts.T(), f.UpdateFriendlyName(ts.db, newSimpleName)) require.Equal(ts.T(), newSimpleName, f.FriendlyName) diff --git a/models/recovery_code.go b/models/recovery_code.go index 244935982..f9e4ca03b 100644 --- a/models/recovery_code.go +++ b/models/recovery_code.go @@ -3,6 +3,7 @@ package models import ( "database/sql" "github.com/gofrs/uuid" + "github.com/netlify/gotrue/crypto" "github.com/netlify/gotrue/storage" "github.com/pkg/errors" "time" @@ -47,3 +48,50 @@ func FindValidRecoveryCodesByUser(tx *storage.Connection, user *User) ([]*Recove } return recoveryCodes, nil } + +// Validate recovery code +func IsRecoveryCodeValid(tx *storage.Connection, user *User, recoveryCode string) (*RecoveryCode, error) { + rc := &RecoveryCode{} + if err := tx.Q().Where("user_id = ? AND used_at IS NULL AND recovery_code = ?", user.ID, recoveryCode).First(&rc); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, nil + } + return nil, nil + } + return rc, nil +} + +// Use and invalidate a recovery code +func (r *RecoveryCode) Consume(tx *storage.Connection) error { + now := time.Now() + r.UsedAt = &now + return tx.UpdateOnly(r, "used_at") +} + +func GenerateBatchOfRecoveryCodes(tx *storage.Connection, user *User) ([]*RecoveryCode, error) { + recoveryCodes := []*RecoveryCode{} + for i := 0; i <= NumRecoveryCodes; i++ { + rc, err := NewRecoveryCode(user, crypto.SecureToken(RecoveryCodeLength)) + if err = tx.Create(rc); err != nil { + return nil, errors.Wrap(err, "error creating recovery code") + } + recoveryCodes = append(recoveryCodes, rc) + } + return recoveryCodes, nil +} + +func ValidateRecoveryCode(tx *storage.Connection, user *User, recoveryCode string) error { + rc, terr := IsRecoveryCodeValid(tx, user, recoveryCode) + if terr != nil { + return terr + } + if rc.RecoveryCode == recoveryCode { + terr = rc.Consume(tx) + if terr != nil { + return terr + } + return nil + } + return errors.New("Invalid code entered") + +} diff --git a/models/recovery_code_test.go b/models/recovery_code_test.go index 98584526c..5117bfb14 100644 --- a/models/recovery_code_test.go +++ b/models/recovery_code_test.go @@ -61,3 +61,17 @@ func (ts *RecoveryCodeTestSuite) createRecoveryCode(u *User) *RecoveryCode { require.NoError(ts.T(), err) return rc } + +// Create Recovery Code +func (ts *RecoveryCodeTestSuite) TestConsumedRecoveryCodesAreNotValid() { + user, err := NewUser("", "", "", "", nil) + err = ts.db.Create(user) + rc := ts.createRecoveryCode(user) + isRCValid, err := IsRecoveryCodeValid(ts.db, user, rc.RecoveryCode) + require.NoError(ts.T(), err) + require.Equal(ts.T(), true, isRCValid) + err = rc.Consume(ts.db) + require.NoError(ts.T(), err) + isRCValid, err = IsRecoveryCodeValid(ts.db, user, rc.RecoveryCode) + require.Equal(ts.T(), false, isRCValid) +} From 091d56c99dc388c810377c8b0d81832550919dfc Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 30 Aug 2022 08:06:46 +0530 Subject: [PATCH 105/180] feat: initial gating of routes requiring 1FA --- api/api.go | 5 +++++ api/auth.go | 9 +++++++++ api/helpers.go | 12 ++++++++++++ 3 files changed, 26 insertions(+) diff --git a/api/api.go b/api/api.go index 0ddec44db..4288a62cc 100644 --- a/api/api.go +++ b/api/api.go @@ -170,6 +170,11 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Post("/challenge", api.ChallengeFactor) r.Delete("/", api.UnenrollFactor) + r.Route("/login", func(r *router) { + r.Use(api.require1FA) + r.Post("/", api.StepUpLogin) + }) + }) }) }) diff --git a/api/auth.go b/api/auth.go index 6c42066c6..ce125ed89 100644 --- a/api/auth.go +++ b/api/auth.go @@ -34,6 +34,15 @@ func (a *API) requireAuthentication(w http.ResponseWriter, r *http.Request) (con return ctx, err } +func (a *API) require1FA(w http.ResponseWriter, r *http.Request) (context.Context, error) { + token, _ := a.extractBearerToken(w, r) + claims := getClaims(r.Context()) + if !(claims.AuthenticatorAssuranceLevel == "aal1") { + return nil, unauthorizedError("User not allowed, 1FA required") + } + return a.parseJWTClaims(token, r, w) +} + func (a *API) requireAdmin(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, error) { // Find the administrative user claims := getClaims(ctx) diff --git a/api/helpers.go b/api/helpers.go index aaa5ca13d..a3f7ea743 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -259,3 +259,15 @@ func getBodyBytes(req *http.Request) ([]byte, error) { return buf, nil } + +// See https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.1800-17.pdf. +// Computes AAL based on list of AMR entries. +func calculateAAL(amr []AMREntry) string { + // TODO(Joel): Modify the checking logic when we have more than 1 2FA method + for _, amrEntry := range amr { + if amrEntry.Method == models.TOTP { + return "aal2" + } + } + return "aal1" +} From b8f486ca767739622c8f412a348c2b4444de7f26 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 30 Aug 2022 09:54:05 +0530 Subject: [PATCH 106/180] chore: add AMR entry --- api/token.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/token.go b/api/token.go index b92aedb1f..28c4b5b63 100644 --- a/api/token.go +++ b/api/token.go @@ -50,6 +50,12 @@ type RefreshTokenGrantParams struct { RefreshToken string `json:"refresh_token"` } +// AMREntry represents a method that a user has logged in together with the corresponding time +type AMREntry struct { + Method string `json:"method"` + Timestamp time.Time `json:"timestamp"` +} + // IdTokenGrantParams are the parameters the IdTokenGrant method accepts type IdTokenGrantParams struct { IdToken string `json:"id_token"` From 93d15495995654a1e30cbde9001f0b04120e4848 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 30 Aug 2022 09:56:15 +0530 Subject: [PATCH 107/180] refactor: update claims --- api/token.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/token.go b/api/token.go index 28c4b5b63..1dd5d34c2 100644 --- a/api/token.go +++ b/api/token.go @@ -21,12 +21,14 @@ import ( // GoTrueClaims is a struct thats used for JWT claims type GoTrueClaims struct { jwt.StandardClaims - Email string `json:"email"` - Phone string `json:"phone"` - AppMetaData map[string]interface{} `json:"app_metadata"` - UserMetaData map[string]interface{} `json:"user_metadata"` - Role string `json:"role"` - SessionId string `json:"session_id"` + Email string `json:"email"` + Phone string `json:"phone"` + AppMetaData map[string]interface{} `json:"app_metadata"` + UserMetaData map[string]interface{} `json:"user_metadata"` + Role string `json:"role"` + AuthenticatorAssuranceLevel string `json:"aal"` + AuthenticationMethodReference []AMREntry `json:"amr"` + SessionId string `json:"session_id"` } // AccessTokenResponse represents an OAuth2 success response From 732fd5c2cb693a58a8ed4ddc387dd6bf4b81ef99 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 30 Aug 2022 15:36:25 +0530 Subject: [PATCH 108/180] test: add initial mfa login tests --- api/mfa_test.go | 67 +++++++++++++++++++++++++++++++++++++++++ models/refresh_token.go | 1 + 2 files changed, 68 insertions(+) diff --git a/api/mfa_test.go b/api/mfa_test.go index dfbbe9981..cedf31e41 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -295,3 +295,70 @@ func (ts *MFATestSuite) TestUnenrollFactor() { } } + +func (ts *MFATestSuite) TestStepUpLogin() { + cases := []struct { + desc string + RecoveryCode string + Code string + IsFirstMFALogin bool + IsOneFAVerified bool + ExpectedHTTPCode int + }{ + { + "Successful login with code", + "", + "123456", + true, + true, + http.StatusOK, + }, + + {"Using both code and recovery code is forbidden", + "123456", + "12345678", + true, + true, + http.StatusForbidden, + }, + { + "Should not return recovery codes if not first login", + "", + "123456", + false, + true, + http.StatusOK, + }, + { + "Successful login with recovery code", + "12345678", + "", + false, + true, + http.StatusOK, + }, + { + "Login without 1FA verified should fail", + "", + "123456", + true, + false, + http.StatusForbidden, + }, + } + for _, v := range cases { + ts.Run(v.desc, func() { + var buffer bytes.Buffer + + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + require.NoError(ts.T(), err) + w := httptest.NewRecorder() + + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/%s/login", u.ID, f.ID), &buffer) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + ts.API.handler.ServeHTTP(w, req) + require.Equal(ts.T(), v.ExpectedHTTPCode, w.Code) + }) + } + +} diff --git a/models/refresh_token.go b/models/refresh_token.go index 5f1bc4630..1761d77a6 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -108,6 +108,7 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok if err != nil { return nil, errors.Wrap(err, "Error generated unique session id") } + // Expire claims here token.SessionId = nulls.NewUUID(session.ID) } From 912e6857f6ac320884cf186ece07d99f7799cbb5 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 30 Aug 2022 18:03:00 +0530 Subject: [PATCH 109/180] refactor: Add sign in method to issueRefreshToken --- api/external.go | 2 +- api/mfa.go | 3 +-- api/signup.go | 2 +- api/token.go | 6 +++--- api/verify.go | 4 ++-- models/mfa_constants.go | 8 ++++++++ models/refresh_token.go | 1 + 7 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/external.go b/api/external.go index ed84659d5..f73c3513a 100644 --- a/api/external.go +++ b/api/external.go @@ -273,7 +273,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re } } - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth) if terr != nil { return oauthError("server_error", terr.Error()) } diff --git a/api/mfa.go b/api/mfa.go index 1b9a396cd..71dc91f07 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -244,8 +244,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if terr = models.NewAuditLogEntry(r, tx, user, models.MFALoginAction, "", nil); terr != nil { return terr } - // TODO(joel): Reinstate the TOTP claim when we add the claims logic to all endpoints - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP) if terr != nil { return terr } diff --git a/api/signup.go b/api/signup.go index a2ea8f5a7..82bb0d8ec 100644 --- a/api/signup.go +++ b/api/signup.go @@ -218,7 +218,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.AutoConfirmSignup) if terr != nil { return terr } diff --git a/api/token.go b/api/token.go index 1dd5d34c2..bb5235510 100644 --- a/api/token.go +++ b/api/token.go @@ -226,7 +226,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri return terr } - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant) if terr != nil { return terr } @@ -515,7 +515,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R } } - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuthIDGrant) if terr != nil { return oauthError("server_error", terr.Error()) } @@ -553,7 +553,7 @@ func generateAccessToken(user *models.User, sessionId string, expiresIn time.Dur return token.SignedString([]byte(secret)) } -func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User) (*AccessTokenResponse, error) { +func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, signInMethod string) (*AccessTokenResponse, error) { config := a.config now := time.Now() diff --git a/api/verify.go b/api/verify.go index 07890cf40..0cd0339cf 100644 --- a/api/verify.go +++ b/api/verify.go @@ -121,7 +121,7 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.EmailVerification) if terr != nil { return terr } @@ -217,7 +217,7 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user) + token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOTPOrGeneratedToken) if terr != nil { return terr } diff --git a/models/mfa_constants.go b/models/mfa_constants.go index abc2f9b6b..c597e719a 100644 --- a/models/mfa_constants.go +++ b/models/mfa_constants.go @@ -3,3 +3,11 @@ package models const NumRecoveryCodes = 8 const RecoveryCodeLength = 8 const DefaultQRSize = 3 + +const OAuth = "oauth" +const OAuthIDGrant = "oauth_id" +const PasswordGrant = "password" +const AutoConfirmSignup = "autoconfirm" + +const EmailVerification = "email_verification" +const SMSOTPOrGeneratedToken = "sms_or_generated_token" diff --git a/models/refresh_token.go b/models/refresh_token.go index 1761d77a6..e5b8f7bda 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -104,6 +104,7 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok token.Parent = storage.NullString(oldToken.Token) token.SessionId = oldToken.SessionId } else { + // TODO(joel): Sessions need to take in the factorID and AMR claims session, err := CreateSession(tx, user) if err != nil { return nil, errors.Wrap(err, "Error generated unique session id") From 6f5353adb5af3955b965ebd8b205e803f5add490 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 30 Aug 2022 18:25:32 +0530 Subject: [PATCH 110/180] fix: distinguish between code logins and recovery code logins --- api/mfa.go | 18 +++++++++++------- models/audit_log_entry.go | 6 ++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 71dc91f07..440f77e5d 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -174,6 +174,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if factor.Status != models.FactorVerifiedState { return unprocessableEntityError("Please attempt a login with a verified factor") } + actionType := "" params := &StepUpLoginParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -213,6 +214,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if !valid { return unauthorizedError("Invalid code entered") } + actionType = models.MFACodeLoginAction } else if params.RecoveryCode != "" { // TODO(suggest): Shorten session duration for sessions arising from recovery code err := a.db.Transaction(func(tx *storage.Connection) error { @@ -235,13 +237,15 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if err != nil { return err } - return unauthorizedError("Invalid code entered") + actionType = models.MFARecoveryCodeLoginAction + } var token *AccessTokenResponse - err = a.db.Transaction(func(tx *storage.Connection) error { - var terr error - if terr = models.NewAuditLogEntry(r, tx, user, models.MFALoginAction, "", nil); terr != nil { + var terr error + + terr = a.db.Transaction(func(tx *storage.Connection) error { + if terr := models.NewAuditLogEntry(r, tx, user, actionType, r.remoteAddr, nil); terr != nil { return terr } token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP) @@ -257,9 +261,9 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if err != nil { return err } - metering.RecordLogin("token", user.ID) - // if user.IsFirstMFALogin(){ - // // Wrap this in a transaction + metering.RecordLogin(actionType, user.ID) + + // if user.IsFirstMFALogin() && actionType != models.RecoveryCodeAction{ // recoveryCodes, err := models.GenerateRecoveryCodesBatch() // return sendJSON(w, http.StatusOK, StepUpLoginResponse{ // token: token diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 3c6ac27c0..afecc9ea6 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -38,7 +38,8 @@ const ( DeleteFactorAction AuditAction = "factor_deleted" DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" UpdateFactorAction AuditAction = "factor_updated" - MFALoginAction AuditAction = "mfa_login" + MFACodeLoginAction AuditAction = "mfa_code_login" + MFARecoveryCodeLoginAction AuditAction = "mfa_recovery_code_login" account auditLogType = "account" team auditLogType = "team" @@ -68,7 +69,8 @@ var ActionLogTypeMap = map[AuditAction]auditLogType{ VerifyFactorAction: factor, DeleteFactorAction: factor, UpdateFactorAction: factor, - MFALoginAction: factor, + MFACodeLoginAction: factor, + MFARecoveryCodeLogin: recoveryCodes, DeleteRecoveryCodesAction: recoveryCodes, } From f750ec363178bad6e8ceba2a0ab7ae79824d0c57 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 31 Aug 2022 11:35:46 +0530 Subject: [PATCH 111/180] fix: add concept of first MFA login --- api/admin_test.go | 2 -- api/mfa.go | 14 ++++++-------- .../20220811173540_add_sessions_table.up.sql | 2 -- ...p.sql => 20220830041349_add_mfa_schema.up.sql} | 15 +++++++++++++++ models/refresh_token.go | 1 - models/sessions.go | 4 +++- models/user.go | 7 +++++++ 7 files changed, 31 insertions(+), 14 deletions(-) rename migrations/{20220607041349_add_mfa_schema.up.sql => 20220830041349_add_mfa_schema.up.sql} (75%) diff --git a/api/admin_test.go b/api/admin_test.go index 10bf6237e..2604de7eb 100644 --- a/api/admin_test.go +++ b/api/admin_test.go @@ -523,7 +523,6 @@ func (ts *AdminTestSuite) TestAdminUserCreateWithDisabledLogin() { // TestAdminUserDeleteRecoveryCodes tests API /admin/users//recovery_codes/ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { - // TODO(Joel): Test case where factor is unverified u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") @@ -554,7 +553,6 @@ func (ts *AdminTestSuite) TestAdminUserDeleteRecoveryCodes() { // TestAdminUserDeleteFactor tests API /admin/users//factor// func (ts *AdminTestSuite) TestAdminUserDeleteFactor() { - // TODO(Joel): Test case where factor is unverified u, err := models.NewUser("123456789", "test-delete@example.com", "test", ts.Config.JWT.Aud, nil) require.NoError(ts.T(), err, "Error making new user") require.NoError(ts.T(), ts.API.db.Create(u), "Error creating user") diff --git a/api/mfa.go b/api/mfa.go index 440f77e5d..8329224de 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -81,7 +81,6 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if params.Issuer == "" { return unprocessableEntityError("Issuer is required") } - // TODO(Joel): Review this portion when email is no longer a primary key key, err := totp.Generate(totp.GenerateOpts{ Issuer: params.Issuer, AccountName: user.GetEmail(), @@ -263,13 +262,12 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { } metering.RecordLogin(actionType, user.ID) - // if user.IsFirstMFALogin() && actionType != models.RecoveryCodeAction{ - // recoveryCodes, err := models.GenerateRecoveryCodesBatch() - // return sendJSON(w, http.StatusOK, StepUpLoginResponse{ - // token: token - // recovery_code: recoveryCodes - // }) - // } + if !user.HasReceivedRecoveryCodes() && actionType != models.RecoveryCodeAction{ + recoveryCodes, err := models.GenerateRecoveryCodesBatch() + return sendJSON(w, http.StatusOK, StepUpLoginResponse{ + recovery_code: recoveryCodes + }) + } return sendJSON(w, http.StatusOK, token) } diff --git a/migrations/20220811173540_add_sessions_table.up.sql b/migrations/20220811173540_add_sessions_table.up.sql index 816ba43c6..390bbdbab 100644 --- a/migrations/20220811173540_add_sessions_table.up.sql +++ b/migrations/20220811173540_add_sessions_table.up.sql @@ -4,8 +4,6 @@ create table if not exists auth.sessions ( user_id uuid not null, created_at timestamptz null, updated_at timestamptz null, - factor_id text null, - amr_claims text [] null, constraint sessions_pkey primary key (id), constraint sessions_user_id_fkey foreign key (user_id) references auth.users(id) on delete cascade diff --git a/migrations/20220607041349_add_mfa_schema.up.sql b/migrations/20220830041349_add_mfa_schema.up.sql similarity index 75% rename from migrations/20220607041349_add_mfa_schema.up.sql rename to migrations/20220830041349_add_mfa_schema.up.sql index 61840e525..91e20f13c 100644 --- a/migrations/20220607041349_add_mfa_schema.up.sql +++ b/migrations/20220830041349_add_mfa_schema.up.sql @@ -44,3 +44,18 @@ CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( CONSTRAINT mfa_recovery_codes_user_id_fkey FOREIGN KEY(user_id) REFERENCES auth.users(id) ON DELETE CASCADE ); comment on table auth.mfa_recovery_codes is 'Auth: stores recovery codes for Multi Factor Authentication'; + +-- Add time at which recovery codes were issued +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS received_recovery_codes_at timestamptz NULL; + +-- Add factor_id and AMR claims to session +CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims( + id uuid NOT NULL, + session_id uuid NOT NULL, + factor_id string NOT NULL, + created_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL, + sign_in_method string NOT NULL, + CONSTRAINT mfa_amr_claims_session_id_fkey FOREIGN KEY(session_id) REFERENCES auth.sessions(id)) ON DELETE CASCADE +); +comment on table auth.mfa_amr_claims is 'Auth: stores authenticator method reference claims for multi factor authentication'; diff --git a/models/refresh_token.go b/models/refresh_token.go index e5b8f7bda..ef8cd2de0 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -109,7 +109,6 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok if err != nil { return nil, errors.Wrap(err, "Error generated unique session id") } - // Expire claims here token.SessionId = nulls.NewUUID(session.ID) } diff --git a/models/sessions.go b/models/sessions.go index ac413cf9e..b9af0a4dd 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -22,7 +22,7 @@ func (Session) TableName() string { return tableName } -func NewSession(user *User) (*Session, error) { +func NewSession(user *User, factorID string) (*Session, error) { id, err := uuid.NewV4() if err != nil { return nil, errors.Wrap(err, "Error generating unique session id") @@ -31,6 +31,7 @@ func NewSession(user *User) (*Session, error) { session := &Session{ ID: id, UserID: user.ID, + FactorID: factorID, } return session, nil } @@ -57,6 +58,7 @@ func FindSessionById(tx *storage.Connection, id uuid.UUID) (*Session, error) { return session, nil } + // Logout deletes all sessions for a user. func Logout(tx *storage.Connection, userId uuid.UUID) error { return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ?", userId).Exec() diff --git a/models/user.go b/models/user.go index 84f303720..b67ef8895 100644 --- a/models/user.go +++ b/models/user.go @@ -63,6 +63,8 @@ type User struct { UpdatedAt time.Time `json:"updated_at" db:"updated_at"` BannedUntil *time.Time `json:"banned_until,omitempty" db:"banned_until"` + RecoveryCodesReceivedAt *time.Time `json:"recovery_codes_recived_at" db:"recovery_codes_received_at"` + DONTUSEINSTANCEID uuid.UUID `json:"-" db:"instance_id"` } @@ -542,3 +544,8 @@ func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection) error { return nil } + + +func (u *User) HasReceivedRecoveryCodes() bool { + return u.RecoveryCodesReceivedAt != nil +} From 9fdc8dae9d410d38d0e3e92fae7cb73dfa62f241 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 31 Aug 2022 14:10:34 +0530 Subject: [PATCH 112/180] chore: add AMRClaims model content --- api/admin.go | 5 +-- api/external.go | 2 +- api/helpers.go | 1 - api/mfa.go | 17 +++++--- api/signup.go | 2 +- api/token.go | 22 ++++++++-- api/verify.go | 4 +- .../20220830041349_add_mfa_schema.up.sql | 1 - models/amr.go | 40 +++++++++++++++++++ models/refresh_token.go | 2 +- models/sessions.go | 24 ++++++++--- models/user.go | 1 - 12 files changed, 95 insertions(+), 26 deletions(-) create mode 100644 models/amr.go diff --git a/api/admin.go b/api/admin.go index 7c7b5d3da..7dc81a304 100644 --- a/api/admin.go +++ b/api/admin.go @@ -472,14 +472,13 @@ func (a *API) adminUserUpdateFactor(w http.ResponseWriter, r *http.Request) erro return terr } } - if params.FactorType != "" { - // TODO(Joel): Update this to check factorType validity when we introduce webauthn + if params.FactorType != "" && !(params.FactorType == models.TOTP || params.FactorType == models.Webauthn) { if terr := factor.UpdateFactorType(tx, params.FactorType); terr != nil { return terr } } if params.FactorStatus != "" { - if !isValidFactorStatus(params.FactorType) { + if !isValidFactorStatus(params.FactorStatus) { return errors.New("Factor Status should be one of the valid factor states: verified, unverified or disabled") } if terr := factor.UpdateStatus(tx, params.FactorStatus); terr != nil { diff --git a/api/external.go b/api/external.go index f73c3513a..1de20fde0 100644 --- a/api/external.go +++ b/api/external.go @@ -273,7 +273,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re } } - token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth) + token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth, nil) if terr != nil { return oauthError("server_error", terr.Error()) } diff --git a/api/helpers.go b/api/helpers.go index a3f7ea743..03e9712b9 100644 --- a/api/helpers.go +++ b/api/helpers.go @@ -263,7 +263,6 @@ func getBodyBytes(req *http.Request) ([]byte, error) { // See https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.1800-17.pdf. // Computes AAL based on list of AMR entries. func calculateAAL(amr []AMREntry) string { - // TODO(Joel): Modify the checking logic when we have more than 1 2FA method for _, amrEntry := range amr { if amrEntry.Method == models.TOTP { return "aal2" diff --git a/api/mfa.go b/api/mfa.go index 8329224de..87339375e 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -215,7 +215,6 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { } actionType = models.MFACodeLoginAction } else if params.RecoveryCode != "" { - // TODO(suggest): Shorten session duration for sessions arising from recovery code err := a.db.Transaction(func(tx *storage.Connection) error { rc, terr := models.IsRecoveryCodeValid(tx, user, params.RecoveryCode) if terr != nil { @@ -247,7 +246,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(r, tx, user, actionType, r.remoteAddr, nil); terr != nil { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP) + token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP, factor.ID) if terr != nil { return terr } @@ -262,11 +261,11 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { } metering.RecordLogin(actionType, user.ID) - if !user.HasReceivedRecoveryCodes() && actionType != models.RecoveryCodeAction{ + if !user.HasReceivedRecoveryCodes() && actionType != models.RecoveryCodeAction { recoveryCodes, err := models.GenerateRecoveryCodesBatch() return sendJSON(w, http.StatusOK, StepUpLoginResponse{ - recovery_code: recoveryCodes - }) + recovery_code: recoveryCodes, + }) } return sendJSON(w, http.StatusOK, token) @@ -293,6 +292,11 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { } return internalServerError("Database error finding Challenge").WithInternalError(err) } + + if challenge.VerifiedAt != nil { + return badRequestError("Challenge has already been verified") + } + valid := totp.Validate(params.Code, factor.SecretKey) if !valid { return unauthorizedError("Invalid TOTP code entered") @@ -334,6 +338,7 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { if err != nil { return err } + // InvalidateSessionWith Exxception return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ Success: fmt.Sprintf("%v", valid), @@ -381,6 +386,8 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { return err } + // Drop all Sessions to AAL1 here + return sendJSON(w, http.StatusOK, &UnenrollFactorResponse{ Success: fmt.Sprintf("%v", valid), }) diff --git a/api/signup.go b/api/signup.go index 82bb0d8ec..e347acd0d 100644 --- a/api/signup.go +++ b/api/signup.go @@ -218,7 +218,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.AutoConfirmSignup) + token, terr = a.issueRefreshToken(ctx, tx, user, models.AutoConfirmSignup, nil) if terr != nil { return terr } diff --git a/api/token.go b/api/token.go index bb5235510..4732c5aa7 100644 --- a/api/token.go +++ b/api/token.go @@ -226,7 +226,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant) + token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant, nil) if terr != nil { return terr } @@ -515,7 +515,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R } } - token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuthIDGrant) + token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuthIDGrant, nil) if terr != nil { return oauthError("server_error", terr.Error()) } @@ -534,6 +534,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return sendJSON(w, http.StatusOK, token) } +// TODO(Joel): Add Factor ID to access token func generateAccessToken(user *models.User, sessionId string, expiresIn time.Duration, secret string) (string, error) { claims := &GoTrueClaims{ StandardClaims: jwt.StandardClaims{ @@ -553,9 +554,10 @@ func generateAccessToken(user *models.User, sessionId string, expiresIn time.Dur return token.SignedString([]byte(secret)) } -func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, signInMethod string) (*AccessTokenResponse, error) { +func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, signInMethod, factorID string) (*AccessTokenResponse, error) { config := a.config - + claims := getClaims(ctx) + const isMFASignInMethod = signInMethod == models.TOTP now := time.Now() user.LastSignInAt = &now @@ -569,6 +571,18 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u return internalServerError("Database error granting user").WithInternalError(terr) } + session := getSession(ctx) + terr = models.AddClaimToSession(session, signInMethod) + if terr != nil { + return terr + } + + if isMFASignInMethod { + if err := session.UpdateAssociatedFactor(factorID); err != nil { + return err + } + } + tokenString, terr = generateAccessToken(user, refreshToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) diff --git a/api/verify.go b/api/verify.go index 0cd0339cf..bceb5bc2d 100644 --- a/api/verify.go +++ b/api/verify.go @@ -121,7 +121,7 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.EmailVerification) + token, terr = a.issueRefreshToken(ctx, tx, user, models.EmailVerification, nil) if terr != nil { return terr } @@ -217,7 +217,7 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOTPOrGeneratedToken) + token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOTPOrGeneratedToken, nil) if terr != nil { return terr } diff --git a/migrations/20220830041349_add_mfa_schema.up.sql b/migrations/20220830041349_add_mfa_schema.up.sql index 91e20f13c..511fe2cad 100644 --- a/migrations/20220830041349_add_mfa_schema.up.sql +++ b/migrations/20220830041349_add_mfa_schema.up.sql @@ -52,7 +52,6 @@ ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS received_recovery_codes_at times CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims( id uuid NOT NULL, session_id uuid NOT NULL, - factor_id string NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, sign_in_method string NOT NULL, diff --git a/models/amr.go b/models/amr.go new file mode 100644 index 000000000..aeb4ce676 --- /dev/null +++ b/models/amr.go @@ -0,0 +1,40 @@ +package models + +import ( + "fmt" +) + +type AMRClaim struct { + ID uuid.UUID `json:"id" db:"id"` + SessionID uuid.UUID `json:"session_id" db:"session_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + SignInMethod string `json:"sign_in_method" db:"sign_in_method"` +} + +func (RecoveryCode) TableName() string { + tableName := "mfa_amr_claims" + return tableName +} + +func NewAMRClaim(sessionID uuid.UUID, signInMethod string) { + id, err := uuid.NewV4() + if err != nil { + return nil, errors.Wrap(err, "Error generating unique id") + } + claim := &AMRClaim{ + ID: id, + SessionID: sesionID, + } + +} + +func AddClaimToSession(session *Session, signInMethod string) error { + claim := NewAMRClaim(session.ID, signInMethod) + return tx.Create(claim) +} + +// Finds all Sessions associated to a factor and deletes them +func DeleteClaimsByFactorID(tx *storage.Connection, factorID string, claimType string) error { + // Join on sessions and calims Find all fclaims assoicated with a given TOTP factor +} diff --git a/models/refresh_token.go b/models/refresh_token.go index ef8cd2de0..40068068d 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -104,7 +104,7 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok token.Parent = storage.NullString(oldToken.Token) token.SessionId = oldToken.SessionId } else { - // TODO(joel): Sessions need to take in the factorID and AMR claims + // TODO(joel): Sessions need to take in the factorID session, err := CreateSession(tx, user) if err != nil { return nil, errors.Wrap(err, "Error generated unique session id") diff --git a/models/sessions.go b/models/sessions.go index b9af0a4dd..1ff3d4130 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -11,10 +11,12 @@ import ( ) type Session struct { - ID uuid.UUID `json:"-" db:"id"` - UserID uuid.UUID `json:"user_id" db:"user_id"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + ID uuid.UUID `json:"-" db:"id"` + UserID uuid.UUID `json:"user_id" db:"user_id"` + CreatedAt time.Time `json:"created_at" db:"created_at"` + UpdatedAt time.Time `json:"updated_at" db:"updated_at"` + FactorID string `json:"factor_id" db:"factor_id"` + AMRClaims []AMRClaim `json:"amr_claims" has_many:"amr_claims"` } func (Session) TableName() string { @@ -29,8 +31,8 @@ func NewSession(user *User, factorID string) (*Session, error) { } session := &Session{ - ID: id, - UserID: user.ID, + ID: id, + UserID: user.ID, FactorID: factorID, } return session, nil @@ -58,6 +60,10 @@ func FindSessionById(tx *storage.Connection, id uuid.UUID) (*Session, error) { return session, nil } +// TODO(Joel): Invalidate all other sessions once MFA is enabled ( A verified factor has been produced). Make use of this in unenroll +func InvalidateSessionsExcludingCurrent(tx *storage.Connection, currentSessionID uuid.UUID) { + return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND session_id != ?", userId, currentSessionID).Exec() +} // Logout deletes all sessions for a user. func Logout(tx *storage.Connection, userId uuid.UUID) error { @@ -67,3 +73,9 @@ func Logout(tx *storage.Connection, userId uuid.UUID) error { func LogoutSession(tx *storage.Connection, sessionId uuid.UUID) error { return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE id = ?", sessionId).Exec() } + +func (*Session) UpdateAssociatedFactor(tx *storage.Connection, factorID string) error { + session.FactorID = factorID + return tx.Update(session) + +} diff --git a/models/user.go b/models/user.go index b67ef8895..5d4303e1c 100644 --- a/models/user.go +++ b/models/user.go @@ -545,7 +545,6 @@ func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection) error { return nil } - func (u *User) HasReceivedRecoveryCodes() bool { return u.RecoveryCodesReceivedAt != nil } From 8593fca955473b7e1866aafc7e8a7301565daad1 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 31 Aug 2022 14:38:34 +0530 Subject: [PATCH 113/180] fix: patch various errors related to method signature --- api/external.go | 2 +- api/mfa.go | 2 +- api/signup.go | 2 +- api/token.go | 6 +++--- api/token_test.go | 6 +++--- api/verify.go | 4 ++-- models/amr.go | 20 ++++++++++++++------ models/audit_log_entry.go | 2 +- models/refresh_token.go | 11 +++++------ models/sessions.go | 14 +++++++------- models/user_test.go | 2 +- 11 files changed, 39 insertions(+), 32 deletions(-) diff --git a/api/external.go b/api/external.go index 1de20fde0..e994297b3 100644 --- a/api/external.go +++ b/api/external.go @@ -273,7 +273,7 @@ func (a *API) internalExternalProviderCallback(w http.ResponseWriter, r *http.Re } } - token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth, nil) + token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuth, "") if terr != nil { return oauthError("server_error", terr.Error()) } diff --git a/api/mfa.go b/api/mfa.go index 87339375e..ef3c4bd6c 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -243,7 +243,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { var terr error terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(r, tx, user, actionType, r.remoteAddr, nil); terr != nil { + if terr := models.NewAuditLogEntry(r, tx, user, actionType, r.RemoteAddr, nil); terr != nil { return terr } token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP, factor.ID) diff --git a/api/signup.go b/api/signup.go index e347acd0d..375ec7296 100644 --- a/api/signup.go +++ b/api/signup.go @@ -218,7 +218,7 @@ func (a *API) Signup(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.AutoConfirmSignup, nil) + token, terr = a.issueRefreshToken(ctx, tx, user, models.AutoConfirmSignup, "") if terr != nil { return terr } diff --git a/api/token.go b/api/token.go index 4732c5aa7..101c04ba1 100644 --- a/api/token.go +++ b/api/token.go @@ -226,7 +226,7 @@ func (a *API) ResourceOwnerPasswordGrant(ctx context.Context, w http.ResponseWri return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant, nil) + token, terr = a.issueRefreshToken(ctx, tx, user, models.PasswordGrant, "") if terr != nil { return terr } @@ -515,7 +515,7 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R } } - token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuthIDGrant, nil) + token, terr = a.issueRefreshToken(ctx, tx, user, models.OAuthIDGrant, "") if terr != nil { return oauthError("server_error", terr.Error()) } @@ -566,7 +566,7 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u err := conn.Transaction(func(tx *storage.Connection) error { var terr error - refreshToken, terr = models.GrantAuthenticatedUser(tx, user) + refreshToken, terr = models.GrantAuthenticatedUser(tx, user, factorID) if terr != nil { return internalServerError("Database error granting user").WithInternalError(terr) } diff --git a/api/token_test.go b/api/token_test.go index 3ff75803c..8403c6a39 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -50,7 +50,7 @@ func (ts *TokenTestSuite) SetupTest() { u.BannedUntil = nil require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u) + ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, nil) require.NoError(ts.T(), err, "Error creating refresh token") } @@ -153,7 +153,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { t := time.Now() u.EmailConfirmedAt = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") - first, err := models.GrantAuthenticatedUser(ts.API.db, u) + first, err := models.GrantAuthenticatedUser(ts.API.db, u, nil) require.NoError(ts.T(), err) second, err := models.GrantRefreshTokenSwap(&http.Request{}, ts.API.db, u, first) require.NoError(ts.T(), err) @@ -245,7 +245,7 @@ func (ts *TokenTestSuite) createBannedUser() *models.User { u.BannedUntil = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test banned user") - ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u) + ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, nil) require.NoError(ts.T(), err, "Error creating refresh token") return u diff --git a/api/verify.go b/api/verify.go index bceb5bc2d..4eee4292d 100644 --- a/api/verify.go +++ b/api/verify.go @@ -121,7 +121,7 @@ func (a *API) verifyGet(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.EmailVerification, nil) + token, terr = a.issueRefreshToken(ctx, tx, user, models.EmailVerification, "") if terr != nil { return terr } @@ -217,7 +217,7 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOTPOrGeneratedToken, nil) + token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOTPOrGeneratedToken, "") if terr != nil { return terr } diff --git a/models/amr.go b/models/amr.go index aeb4ce676..a5f6f3c65 100644 --- a/models/amr.go +++ b/models/amr.go @@ -1,7 +1,10 @@ package models import ( - "fmt" + "github.com/gofrs/uuid" + "github.com/netlify/gotrue/storage" + "github.com/pkg/errors" + "time" ) type AMRClaim struct { @@ -12,29 +15,34 @@ type AMRClaim struct { SignInMethod string `json:"sign_in_method" db:"sign_in_method"` } -func (RecoveryCode) TableName() string { +func (AMRClaim) TableName() string { tableName := "mfa_amr_claims" return tableName } -func NewAMRClaim(sessionID uuid.UUID, signInMethod string) { +func NewAMRClaim(sessionID uuid.UUID, signInMethod string) (*AMRClaim, error) { id, err := uuid.NewV4() if err != nil { return nil, errors.Wrap(err, "Error generating unique id") } claim := &AMRClaim{ ID: id, - SessionID: sesionID, + SessionID: sessionID, } + return claim, nil } -func AddClaimToSession(session *Session, signInMethod string) error { - claim := NewAMRClaim(session.ID, signInMethod) +func AddClaimToSession(tx *storage.Connection, session *Session, signInMethod string) error { + claim, err := NewAMRClaim(session.ID, signInMethod) + if err != nil { + return err + } return tx.Create(claim) } // Finds all Sessions associated to a factor and deletes them func DeleteClaimsByFactorID(tx *storage.Connection, factorID string, claimType string) error { + return errors.New("Unimplemented") // Join on sessions and calims Find all fclaims assoicated with a given TOTP factor } diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index afecc9ea6..2005076e4 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -70,7 +70,7 @@ var ActionLogTypeMap = map[AuditAction]auditLogType{ DeleteFactorAction: factor, UpdateFactorAction: factor, MFACodeLoginAction: factor, - MFARecoveryCodeLogin: recoveryCodes, + MFARecoveryCodeLoginAction: recoveryCodes, DeleteRecoveryCodesAction: recoveryCodes, } diff --git a/models/refresh_token.go b/models/refresh_token.go index 40068068d..606c72176 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -37,8 +37,8 @@ func (RefreshToken) TableName() string { } // GrantAuthenticatedUser creates a refresh token for the provided user. -func GrantAuthenticatedUser(tx *storage.Connection, user *User) (*RefreshToken, error) { - return createRefreshToken(tx, user, nil) +func GrantAuthenticatedUser(tx *storage.Connection, user *User, factorID string) (*RefreshToken, error) { + return createRefreshToken(tx, user, nil, factorID) } // GrantRefreshTokenSwap swaps a refresh token for a new one, revoking the provided token. @@ -54,7 +54,7 @@ func GrantRefreshTokenSwap(r *http.Request, tx *storage.Connection, user *User, if terr = tx.UpdateOnly(token, "revoked"); terr != nil { return terr } - newToken, terr = createRefreshToken(rtx, user, token) + newToken, terr = createRefreshToken(rtx, user, token, "") return terr }) return newToken, err @@ -94,7 +94,7 @@ func GetValidChildToken(tx *storage.Connection, token *RefreshToken) (*RefreshTo return refreshToken, nil } -func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshToken) (*RefreshToken, error) { +func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshToken, factorID string) (*RefreshToken, error) { token := &RefreshToken{ UserID: user.ID, Token: crypto.SecureToken(), @@ -104,8 +104,7 @@ func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshTok token.Parent = storage.NullString(oldToken.Token) token.SessionId = oldToken.SessionId } else { - // TODO(joel): Sessions need to take in the factorID - session, err := CreateSession(tx, user) + session, err := CreateSession(tx, user, factorID) if err != nil { return nil, errors.Wrap(err, "Error generated unique session id") } diff --git a/models/sessions.go b/models/sessions.go index 1ff3d4130..356fd67b1 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -38,8 +38,8 @@ func NewSession(user *User, factorID string) (*Session, error) { return session, nil } -func CreateSession(tx *storage.Connection, user *User) (*Session, error) { - session, err := NewSession(user) +func CreateSession(tx *storage.Connection, user *User, factorID string) (*Session, error) { + session, err := NewSession(user, factorID) if err != nil { return nil, err } @@ -61,8 +61,8 @@ func FindSessionById(tx *storage.Connection, id uuid.UUID) (*Session, error) { } // TODO(Joel): Invalidate all other sessions once MFA is enabled ( A verified factor has been produced). Make use of this in unenroll -func InvalidateSessionsExcludingCurrent(tx *storage.Connection, currentSessionID uuid.UUID) { - return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND session_id != ?", userId, currentSessionID).Exec() +func InvalidateSessionsExcludingCurrent(tx *storage.Connection, currentSessionID uuid.UUID, userID uuid.UUID) error { + return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND session_id != ?", userID, currentSessionID).Exec() } // Logout deletes all sessions for a user. @@ -74,8 +74,8 @@ func LogoutSession(tx *storage.Connection, sessionId uuid.UUID) error { return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE id = ?", sessionId).Exec() } -func (*Session) UpdateAssociatedFactor(tx *storage.Connection, factorID string) error { - session.FactorID = factorID - return tx.Update(session) +func (s *Session) UpdateAssociatedFactor(tx *storage.Connection, factorID string) error { + s.FactorID = factorID + return tx.Update(s) } diff --git a/models/user_test.go b/models/user_test.go index 8ff308a49..67951b992 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -148,7 +148,7 @@ func (ts *UserTestSuite) TestFindUserByRecoveryToken() { func (ts *UserTestSuite) TestFindUserWithRefreshToken() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, nil) require.NoError(ts.T(), err) n, nr, err := FindUserWithRefreshToken(ts.db, r.Token) From 32224ff00e67cbba799a81240b6ec0c9bb9c3121 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 31 Aug 2022 15:06:52 +0530 Subject: [PATCH 114/180] fix: types for audit action logging --- api/mfa.go | 21 +++++++++++++-------- api/token.go | 7 +++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index ef3c4bd6c..0f2e325db 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -213,7 +213,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if !valid { return unauthorizedError("Invalid code entered") } - actionType = models.MFACodeLoginAction + actionType = string(models.MFACodeLoginAction) } else if params.RecoveryCode != "" { err := a.db.Transaction(func(tx *storage.Connection) error { rc, terr := models.IsRecoveryCodeValid(tx, user, params.RecoveryCode) @@ -235,15 +235,17 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if err != nil { return err } - actionType = models.MFARecoveryCodeLoginAction + actionType = string(models.MFARecoveryCodeLoginAction) } var token *AccessTokenResponse + var recoveryCodes []*models.RecoveryCode var terr error + shouldReturnRecoveryCodes := !user.HasReceivedRecoveryCodes() && actionType != string(models.MFARecoveryCodeLoginAction) terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(r, tx, user, actionType, r.RemoteAddr, nil); terr != nil { + if terr := models.NewAuditLogEntry(r, tx, user, models.AuditAction(actionType), r.RemoteAddr, nil); terr != nil { return terr } token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP, factor.ID) @@ -254,6 +256,11 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if terr = a.setCookieTokens(config, token, false, w); terr != nil { return internalServerError("Failed to set JWT cookie. %s", terr) } + recoveryCodes, err = models.GenerateBatchOfRecoveryCodes(tx, user) + if err != nil { + return err + } + return nil }) if err != nil { @@ -261,11 +268,9 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { } metering.RecordLogin(actionType, user.ID) - if !user.HasReceivedRecoveryCodes() && actionType != models.RecoveryCodeAction { - recoveryCodes, err := models.GenerateRecoveryCodesBatch() - return sendJSON(w, http.StatusOK, StepUpLoginResponse{ - recovery_code: recoveryCodes, - }) + // TODO(Joel): Find a way to refactor this in a transaction + if shouldReturnRecoveryCodes { + return sendJSON(w, http.StatusOK, recoveryCodes) } return sendJSON(w, http.StatusOK, token) diff --git a/api/token.go b/api/token.go index 101c04ba1..5a076f5b6 100644 --- a/api/token.go +++ b/api/token.go @@ -556,8 +556,7 @@ func generateAccessToken(user *models.User, sessionId string, expiresIn time.Dur func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, signInMethod, factorID string) (*AccessTokenResponse, error) { config := a.config - claims := getClaims(ctx) - const isMFASignInMethod = signInMethod == models.TOTP + isMFASignInMethod := signInMethod == models.TOTP now := time.Now() user.LastSignInAt = &now @@ -572,13 +571,13 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u } session := getSession(ctx) - terr = models.AddClaimToSession(session, signInMethod) + terr = models.AddClaimToSession(tx, session, signInMethod) if terr != nil { return terr } if isMFASignInMethod { - if err := session.UpdateAssociatedFactor(factorID); err != nil { + if err := session.UpdateAssociatedFactor(tx, factorID); err != nil { return err } } From ad811ade531dbb0c770b88c2cd2d09c250296d8b Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 31 Aug 2022 15:11:00 +0530 Subject: [PATCH 115/180] fix: patch sql syntax errors --- migrations/20220830041349_add_mfa_schema.up.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/migrations/20220830041349_add_mfa_schema.up.sql b/migrations/20220830041349_add_mfa_schema.up.sql index 511fe2cad..e90853285 100644 --- a/migrations/20220830041349_add_mfa_schema.up.sql +++ b/migrations/20220830041349_add_mfa_schema.up.sql @@ -54,7 +54,8 @@ CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims( session_id uuid NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, - sign_in_method string NOT NULL, - CONSTRAINT mfa_amr_claims_session_id_fkey FOREIGN KEY(session_id) REFERENCES auth.sessions(id)) ON DELETE CASCADE + sign_in_method text NOT NULL, + CONSTRAINT mfa_amr_claims_session_id_pkey UNIQUE(session_id, id), + CONSTRAINT mfa_amr_claims_session_id_fkey FOREIGN KEY(session_id) REFERENCES auth.sessions(id) ON DELETE CASCADE ); comment on table auth.mfa_amr_claims is 'Auth: stores authenticator method reference claims for multi factor authentication'; From ac0c3c8c428f622884b94b1bbb8e51b778958c85 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Wed, 31 Aug 2022 17:16:40 +0530 Subject: [PATCH 116/180] chore: patch tests for stepup login --- api/mfa_test.go | 13 ++++++++++++- api/token.go | 7 +++++++ api/token_test.go | 6 +++--- migrations/20220830041349_add_mfa_schema.up.sql | 4 +++- models/amr.go | 5 +++-- models/refresh_token_test.go | 6 +++--- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index cedf31e41..1a124ffa7 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -349,7 +349,18 @@ func (ts *MFATestSuite) TestStepUpLogin() { for _, v := range cases { ts.Run(v.desc, func() { var buffer bytes.Buffer - + u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) + emailValue, err := u.Email.Value() + require.NoError(ts.T(), err) + testEmail := emailValue.(string) + testDomain := strings.Split(testEmail, "@")[1] + // Set factor secret + _, err = totp.Generate(totp.GenerateOpts{ + Issuer: testDomain, + AccountName: testEmail, + }) + factors, err := models.FindFactorsByUser(ts.API.db, u) + f := factors[0] token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) w := httptest.NewRecorder() diff --git a/api/token.go b/api/token.go index 5a076f5b6..e016a7ad2 100644 --- a/api/token.go +++ b/api/token.go @@ -571,6 +571,13 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u } session := getSession(ctx) + if session == nil { + err, session := models.FindSessionById(tx, refreshToken.SessionId) + if err != nil { + return err + } + } + // What happens if session is nil terr = models.AddClaimToSession(tx, session, signInMethod) if terr != nil { return terr diff --git a/api/token_test.go b/api/token_test.go index 8403c6a39..f4f1790a2 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -50,7 +50,7 @@ func (ts *TokenTestSuite) SetupTest() { u.BannedUntil = nil require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, nil) + ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, "") require.NoError(ts.T(), err, "Error creating refresh token") } @@ -153,7 +153,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { t := time.Now() u.EmailConfirmedAt = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") - first, err := models.GrantAuthenticatedUser(ts.API.db, u, nil) + first, err := models.GrantAuthenticatedUser(ts.API.db, u, "") require.NoError(ts.T(), err) second, err := models.GrantRefreshTokenSwap(&http.Request{}, ts.API.db, u, first) require.NoError(ts.T(), err) @@ -245,7 +245,7 @@ func (ts *TokenTestSuite) createBannedUser() *models.User { u.BannedUntil = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test banned user") - ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, nil) + ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, "") require.NoError(ts.T(), err, "Error creating refresh token") return u diff --git a/migrations/20220830041349_add_mfa_schema.up.sql b/migrations/20220830041349_add_mfa_schema.up.sql index e90853285..6ae20a97d 100644 --- a/migrations/20220830041349_add_mfa_schema.up.sql +++ b/migrations/20220830041349_add_mfa_schema.up.sql @@ -46,7 +46,9 @@ CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( comment on table auth.mfa_recovery_codes is 'Auth: stores recovery codes for Multi Factor Authentication'; -- Add time at which recovery codes were issued -ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS received_recovery_codes_at timestamptz NULL; +ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS recovery_codes_received_at timestamptz NULL; +-- Add factor_id to sessions +ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS factor_id text NULL; -- Add factor_id and AMR claims to session CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims( diff --git a/models/amr.go b/models/amr.go index a5f6f3c65..b1efa823b 100644 --- a/models/amr.go +++ b/models/amr.go @@ -26,8 +26,9 @@ func NewAMRClaim(sessionID uuid.UUID, signInMethod string) (*AMRClaim, error) { return nil, errors.Wrap(err, "Error generating unique id") } claim := &AMRClaim{ - ID: id, - SessionID: sessionID, + ID: id, + SessionID: sessionID, + SignInMethod: signInMethod, } return claim, nil diff --git a/models/refresh_token_test.go b/models/refresh_token_test.go index cbf32af25..47f0ca880 100644 --- a/models/refresh_token_test.go +++ b/models/refresh_token_test.go @@ -37,7 +37,7 @@ func TestRefreshToken(t *testing.T) { func (ts *RefreshTokenTestSuite) TestGrantAuthenticatedUser() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, "") require.NoError(ts.T(), err) require.NotEmpty(ts.T(), r.Token) @@ -46,7 +46,7 @@ func (ts *RefreshTokenTestSuite) TestGrantAuthenticatedUser() { func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, "") require.NoError(ts.T(), err) s, err := GrantRefreshTokenSwap(&http.Request{}, ts.db, u, r) @@ -64,7 +64,7 @@ func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() { func (ts *RefreshTokenTestSuite) TestLogout() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, "") require.NoError(ts.T(), err) require.NoError(ts.T(), Logout(ts.db, u.ID)) From 8572a3b889341107b635c56acf14c5c6d3ed6264 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 1 Sep 2022 12:52:48 +0530 Subject: [PATCH 117/180] fix: update number of user fields --- api/mfa_test.go | 2 ++ api/signup_test.go | 2 +- api/token.go | 38 +++++++++++++++++++------------------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index 1a124ffa7..26b73e064 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -361,6 +361,7 @@ func (ts *MFATestSuite) TestStepUpLogin() { }) factors, err := models.FindFactorsByUser(ts.API.db, u) f := factors[0] + // Sign in with regular email password token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -369,6 +370,7 @@ func (ts *MFATestSuite) TestStepUpLogin() { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), v.ExpectedHTTPCode, w.Code) + // Assertion logic }) } diff --git a/api/signup_test.go b/api/signup_test.go index 6e43cbe57..543556cfd 100644 --- a/api/signup_test.go +++ b/api/signup_test.go @@ -110,7 +110,7 @@ func (ts *SignupTestSuite) TestWebhookTriggered() { u, ok := data["user"].(map[string]interface{}) require.True(ok) - assert.Len(u, 11) + assert.Len(u, 12) assert.Equal("authenticated", u["aud"]) assert.Equal("authenticated", u["role"]) assert.Equal("test@example.com", u["email"]) diff --git a/api/token.go b/api/token.go index e016a7ad2..14973a961 100644 --- a/api/token.go +++ b/api/token.go @@ -556,7 +556,7 @@ func generateAccessToken(user *models.User, sessionId string, expiresIn time.Dur func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, signInMethod, factorID string) (*AccessTokenResponse, error) { config := a.config - isMFASignInMethod := signInMethod == models.TOTP + // isMFASignInMethod := signInMethod == models.TOTP now := time.Now() user.LastSignInAt = &now @@ -570,24 +570,24 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u return internalServerError("Database error granting user").WithInternalError(terr) } - session := getSession(ctx) - if session == nil { - err, session := models.FindSessionById(tx, refreshToken.SessionId) - if err != nil { - return err - } - } - // What happens if session is nil - terr = models.AddClaimToSession(tx, session, signInMethod) - if terr != nil { - return terr - } - - if isMFASignInMethod { - if err := session.UpdateAssociatedFactor(tx, factorID); err != nil { - return err - } - } + // session := getSession(ctx) + // if session == nil { + // err, session := models.FindSessionById(tx, refreshToken.SessionId) + // if err != nil { + // return err + // } + // } + // // What happens if session is nil + // terr = models.AddClaimToSession(tx, session, signInMethod) + // if terr != nil { + // return terr + // } + + // if isMFASignInMethod { + // if err := session.UpdateAssociatedFactor(tx, factorID); err != nil { + // return err + // } + // } tokenString, terr = generateAccessToken(user, refreshToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) if terr != nil { From 1a4fa1f5a108e98c70a5a907e58a955d6cd36364 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 1 Sep 2022 21:07:51 +0530 Subject: [PATCH 118/180] refactor: modify access token generation --- api/audit_test.go | 2 +- api/invite_test.go | 2 +- api/mfa.go | 1 + api/mfa_test.go | 10 +++++----- api/phone_test.go | 2 +- api/token.go | 29 ++++++++++++++++++++--------- api/user_test.go | 10 +++++----- api/verify_test.go | 2 +- 8 files changed, 35 insertions(+), 23 deletions(-) diff --git a/api/audit_test.go b/api/audit_test.go index ad868de3a..4a140a2f2 100644 --- a/api/audit_test.go +++ b/api/audit_test.go @@ -48,7 +48,7 @@ func (ts *AuditTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} diff --git a/api/invite_test.go b/api/invite_test.go index 52a8e0924..53150cce1 100644 --- a/api/invite_test.go +++ b/api/invite_test.go @@ -58,7 +58,7 @@ func (ts *InviteTestSuite) makeSuperAdmin(email string) string { u.Role = "supabase_admin" - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err, "Error generating access token") p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}} diff --git a/api/mfa.go b/api/mfa.go index 0f2e325db..761679ffb 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -248,6 +248,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if terr := models.NewAuditLogEntry(r, tx, user, models.AuditAction(actionType), r.RemoteAddr, nil); terr != nil { return terr } + token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP, factor.ID) if terr != nil { return terr diff --git a/api/mfa_test.go b/api/mfa_test.go index 26b73e064..e3e10a67c 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -92,7 +92,7 @@ func (ts *MFATestSuite) TestEnrollFactor() { user, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) ts.Require().NoError(err) - token, err := generateAccessToken(user, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(user, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -121,7 +121,7 @@ func (ts *MFATestSuite) TestChallengeFactor() { f, err := models.FindFactorByFactorID(ts.API.db, "testFactorID") require.NoError(ts.T(), err) - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err, "Error generating access token") var buffer bytes.Buffer @@ -206,7 +206,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { "code": code, })) - token, err := generateAccessToken(user, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(user, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) w := httptest.NewRecorder() @@ -272,7 +272,7 @@ func (ts *MFATestSuite) TestUnenrollFactor() { var buffer bytes.Buffer - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) @@ -362,7 +362,7 @@ func (ts *MFATestSuite) TestStepUpLogin() { factors, err := models.FindFactorsByUser(ts.API.db, u) f := factors[0] // Sign in with regular email password - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) w := httptest.NewRecorder() diff --git a/api/phone_test.go b/api/phone_test.go index 5f5f9cc68..a17f8e04b 100644 --- a/api/phone_test.go +++ b/api/phone_test.go @@ -126,7 +126,7 @@ func (ts *PhoneTestSuite) TestMissingSmsProviderConfig() { u.PhoneConfirmedAt = &now require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user") - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) cases := []struct { diff --git a/api/token.go b/api/token.go index 0be61678e..1a16a22df 100644 --- a/api/token.go +++ b/api/token.go @@ -333,7 +333,7 @@ func (a *API) RefreshTokenGrant(ctx context.Context, w http.ResponseWriter, r *h } } - tokenString, terr = generateAccessToken(user, newToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + tokenString, terr = generateAccessToken(user, newToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret, nil, "") if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } @@ -535,19 +535,29 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R } // TODO(Joel): Add Factor ID to access token -func generateAccessToken(user *models.User, sessionId string, expiresIn time.Duration, secret string) (string, error) { +func generateAccessToken(user *models.User, sessionId string, expiresIn time.Duration, secret string, oldClaims *GoTrueClaims, signInMethod string) (string, error) { + amr := []AMREntry{} + aal := "aal1" + if oldClaims != nil && oldClaims.AuthenticationMethodReference != nil { + amr = oldClaims.AuthenticationMethodReference + } + entry := AMREntry{Method: signInMethod, Timestamp: time.Now()} + amr = append(amr, entry) + aal = calculateAAL(amr) claims := &GoTrueClaims{ StandardClaims: jwt.StandardClaims{ Subject: user.ID.String(), Audience: user.Aud, ExpiresAt: time.Now().Add(expiresIn).Unix(), }, - Email: user.GetEmail(), - Phone: user.GetPhone(), - AppMetaData: user.AppMetaData, - UserMetaData: user.UserMetaData, - Role: user.Role, - SessionId: sessionId, + Email: user.GetEmail(), + Phone: user.GetPhone(), + AppMetaData: user.AppMetaData, + UserMetaData: user.UserMetaData, + Role: user.Role, + SessionId: sessionId, + AuthenticatorAssuranceLevel: aal, + AuthenticationMethodReference: amr, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) @@ -562,6 +572,7 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u var tokenString string var refreshToken *models.RefreshToken + currentClaims := getClaims(ctx) err := conn.Transaction(func(tx *storage.Connection) error { var terr error @@ -590,7 +601,7 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u // } // } - tokenString, terr = generateAccessToken(user, refreshToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret) + tokenString, terr = generateAccessToken(user, refreshToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret, currentClaims, signInMethod) if terr != nil { return internalServerError("error generating jwt token").WithInternalError(terr) } diff --git a/api/user_test.go b/api/user_test.go index 83163bd1c..1b60045f1 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -47,7 +47,7 @@ func (ts *UserTestSuite) SetupTest() { func (ts *UserTestSuite) TestUserGet() { u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) require.NoError(ts.T(), err, "Error finding user") - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err, "Error generating access token") req := httptest.NewRequest(http.MethodGet, "http://localhost/user", nil) @@ -111,7 +111,7 @@ func (ts *UserTestSuite) TestUserUpdateEmail() { require.NoError(ts.T(), u.SetPhone(ts.API.db, c.userData["phone"]), "Error setting user phone") require.NoError(ts.T(), ts.API.db.Create(u), "Error saving test user") - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err, "Error generating access token") var buffer bytes.Buffer @@ -170,7 +170,7 @@ func (ts *UserTestSuite) TestUserUpdatePhoneAutoconfirmEnabled() { for _, c := range cases { ts.Run(c.desc, func() { - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err, "Error generating access token") var buffer bytes.Buffer @@ -244,7 +244,7 @@ func (ts *UserTestSuite) TestUserUpdatePassword() { req := httptest.NewRequest(http.MethodPut, "http://localhost/user", &buffer) req.Header.Set("Content-Type", "application/json") - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) @@ -272,7 +272,7 @@ func (ts *UserTestSuite) TestUserUpdatePasswordReauthentication() { u.EmailConfirmedAt = &now require.NoError(ts.T(), ts.API.db.Update(u), "Error updating new test user") - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) // request for reauthentication nonce diff --git a/api/verify_test.go b/api/verify_test.go index 3b17a75cc..560940d6f 100644 --- a/api/verify_test.go +++ b/api/verify_test.go @@ -103,7 +103,7 @@ func (ts *VerifyTestSuite) TestVerifySecureEmailChange() { req.Header.Set("Content-Type", "application/json") // Generate access token for request - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret) + token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) From 02f4bbbdd4e8c6da9176972a65320c82c4b59761 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 1 Sep 2022 21:38:53 +0530 Subject: [PATCH 119/180] fix: step up login --- api/mfa.go | 2 +- api/mfa_test.go | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 761679ffb..1e10d4c57 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -190,7 +190,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) if err != nil { if models.IsNotFoundError(err) { - return notFoundError(err.Error()) + return internalServerError(err.Error()) } return internalServerError("Database error finding Challenge").WithInternalError(err) } diff --git a/api/mfa_test.go b/api/mfa_test.go index e3e10a67c..79fb490bd 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -355,17 +355,35 @@ func (ts *MFATestSuite) TestStepUpLogin() { testEmail := emailValue.(string) testDomain := strings.Split(testEmail, "@")[1] // Set factor secret - _, err = totp.Generate(totp.GenerateOpts{ + key, err := totp.Generate(totp.GenerateOpts{ Issuer: testDomain, AccountName: testEmail, }) + sharedSecret := key.Secret() + factors, err := models.FindFactorsByUser(ts.API.db, u) f := factors[0] + f.SecretKey = sharedSecret + require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") + + err = f.UpdateStatus(ts.API.db, models.FactorVerifiedState) + require.NoError(ts.T(), err) + code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) + require.NoError(ts.T(), err) + c, err := models.NewChallenge(f) + require.NoError(ts.T(), err, "Error creating test Challenge model") + require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") // Sign in with regular email password token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") require.NoError(ts.T(), err) w := httptest.NewRecorder() + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ + "challenge_id": c.ID, + "code": code, + "recovery_code": "", + })) + req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/%s/login", u.ID, f.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) From 04f838dc04316c21797dea830a60204332b53da7 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 1 Sep 2022 21:53:17 +0530 Subject: [PATCH 120/180] fix: get tests to pass --- api/mfa_test.go | 82 +++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/api/mfa_test.go b/api/mfa_test.go index 79fb490bd..c68c71d07 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -298,53 +298,52 @@ func (ts *MFATestSuite) TestUnenrollFactor() { func (ts *MFATestSuite) TestStepUpLogin() { cases := []struct { - desc string - RecoveryCode string - Code string - IsFirstMFALogin bool - IsOneFAVerified bool - ExpectedHTTPCode int + desc string + IsRecoveryCodeLogin bool + IsCodeLogin bool + IsFirstMFALogin bool + IsOneFAVerified bool + ExpectedHTTPCode int }{ { "Successful login with code", - "", - "123456", + false, + true, true, true, http.StatusOK, }, {"Using both code and recovery code is forbidden", - "123456", - "12345678", true, true, - http.StatusForbidden, - }, - { - "Should not return recovery codes if not first login", - "", - "123456", - false, true, - http.StatusOK, - }, - { - "Successful login with recovery code", - "12345678", - "", - false, true, - http.StatusOK, - }, - { - "Login without 1FA verified should fail", - "", - "123456", - true, - false, - http.StatusForbidden, + http.StatusUnprocessableEntity, }, + // { + // "Should not return recovery codes if not first login", + // false, + // true, + // false, + // true, + // http.StatusOK, + // }, + // { + // "Successful login with recovery code", + // false, + // true, + // false, + // true, + // http.StatusOK, + // }, + // { + // "Login without 1FA verified should fail", + // "", + // true, + // false, + // http.StatusForbidden, + // }, } for _, v := range cases { ts.Run(v.desc, func() { @@ -368,8 +367,7 @@ func (ts *MFATestSuite) TestStepUpLogin() { err = f.UpdateStatus(ts.API.db, models.FactorVerifiedState) require.NoError(ts.T(), err) - code, err := totp.GenerateCode(sharedSecret, time.Now().UTC()) - require.NoError(ts.T(), err) + c, err := models.NewChallenge(f) require.NoError(ts.T(), err, "Error creating test Challenge model") require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") @@ -378,10 +376,22 @@ func (ts *MFATestSuite) TestStepUpLogin() { require.NoError(ts.T(), err) w := httptest.NewRecorder() + //TODO(Joel): Patch this portion to properly check all cases surrounding login + recoveryCode := "" + code := "" + + if v.IsRecoveryCodeLogin { + recoveryCode = "123456" + } + if v.IsCodeLogin { + code, err = totp.GenerateCode(sharedSecret, time.Now().UTC()) + require.NoError(ts.T(), err) + } + require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ "challenge_id": c.ID, "code": code, - "recovery_code": "", + "recovery_code": recoveryCode, })) req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/%s/login", u.ID, f.ID), &buffer) From 7bff6665cb59f3240c89d8960e256f937f3316e4 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Thu, 1 Sep 2022 22:01:17 +0530 Subject: [PATCH 121/180] fix: patch gosec errors --- api/verify.go | 2 +- models/instance.go | 4 +++- models/mfa_constants.go | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/api/verify.go b/api/verify.go index 4eee4292d..abd45dc90 100644 --- a/api/verify.go +++ b/api/verify.go @@ -217,7 +217,7 @@ func (a *API) verifyPost(w http.ResponseWriter, r *http.Request) error { return terr } - token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOTPOrGeneratedToken, "") + token, terr = a.issueRefreshToken(ctx, tx, user, models.SMSOrGeneratedLink, "") if terr != nil { return terr } diff --git a/models/instance.go b/models/instance.go index ef85a8b1e..cedf1fe42 100644 --- a/models/instance.go +++ b/models/instance.go @@ -35,7 +35,9 @@ func (i *Instance) Config() (*conf.GlobalConfiguration, error) { baseConf := &conf.GlobalConfiguration{} *baseConf = *i.BaseConfig - baseConf.ApplyDefaults() + if err := baseConf.ApplyDefaults(); err != nil { + return nil, nil + } return baseConf, nil } diff --git a/models/mfa_constants.go b/models/mfa_constants.go index c597e719a..5cb7faef6 100644 --- a/models/mfa_constants.go +++ b/models/mfa_constants.go @@ -10,4 +10,4 @@ const PasswordGrant = "password" const AutoConfirmSignup = "autoconfirm" const EmailVerification = "email_verification" -const SMSOTPOrGeneratedToken = "sms_or_generated_token" +const SMSOrGeneratedLink = "sms_or_generated_link" From 8399e02a43a8fbb6f4061e298239b477f6c6c912 Mon Sep 17 00:00:00 2001 From: Joel Lee Date: Thu, 1 Sep 2022 22:10:15 +0530 Subject: [PATCH 122/180] chore: pull in master into mfa (#660) * refactor: `TruncateAll` for better readability (#650) * Remove unused GenerateEmailOtp function (#655) * Remove unused function * chore: remove helper function * Update crypto.go * refactor: configuration with validation (#648) * feat: use proper ip address (#649) * refactor: add `GrantParams` for issuing refresh tokens (#659) * fix: remove instance.go Co-authored-by: Stojan Dimitrovski Co-authored-by: joel@joellee.org --- api/token.go | 2 +- api/token_test.go | 6 +-- conf/configuration.go | 75 ++++++++++++++++++++++++++----- conf/tracing.go | 4 ++ crypto/crypto.go | 19 -------- logger/log.go | 3 +- models/connection.go | 21 ++++++--- models/instance.go | 87 ------------------------------------ models/refresh_token.go | 13 ++++-- models/refresh_token_test.go | 6 +-- models/user_test.go | 2 +- security/hcaptcha.go | 3 +- utilities/request.go | 35 +++++++++++++++ utilities/request_test.go | 87 ++++++++++++++++++++++++++++++++++++ 14 files changed, 224 insertions(+), 139 deletions(-) delete mode 100644 models/instance.go create mode 100644 utilities/request.go create mode 100644 utilities/request_test.go diff --git a/api/token.go b/api/token.go index b92aedb1f..cd5356700 100644 --- a/api/token.go +++ b/api/token.go @@ -556,7 +556,7 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u err := conn.Transaction(func(tx *storage.Connection) error { var terr error - refreshToken, terr = models.GrantAuthenticatedUser(tx, user) + refreshToken, terr = models.GrantAuthenticatedUser(tx, user, models.GrantParams{}) if terr != nil { return internalServerError("Database error granting user").WithInternalError(terr) } diff --git a/api/token_test.go b/api/token_test.go index 3ff75803c..c8d363776 100644 --- a/api/token_test.go +++ b/api/token_test.go @@ -50,7 +50,7 @@ func (ts *TokenTestSuite) SetupTest() { u.BannedUntil = nil require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test user") - ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u) + ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{}) require.NoError(ts.T(), err, "Error creating refresh token") } @@ -153,7 +153,7 @@ func (ts *TokenTestSuite) TestTokenRefreshTokenRotation() { t := time.Now() u.EmailConfirmedAt = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving foo user") - first, err := models.GrantAuthenticatedUser(ts.API.db, u) + first, err := models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{}) require.NoError(ts.T(), err) second, err := models.GrantRefreshTokenSwap(&http.Request{}, ts.API.db, u, first) require.NoError(ts.T(), err) @@ -245,7 +245,7 @@ func (ts *TokenTestSuite) createBannedUser() *models.User { u.BannedUntil = &t require.NoError(ts.T(), ts.API.db.Create(u), "Error saving new test banned user") - ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u) + ts.RefreshToken, err = models.GrantAuthenticatedUser(ts.API.db, u, models.GrantParams{}) require.NoError(ts.T(), err, "Error creating refresh token") return u diff --git a/conf/configuration.go b/conf/configuration.go index 821aad986..bb7f35fd9 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -2,6 +2,7 @@ package conf import ( "errors" + "net/url" "os" "time" @@ -37,6 +38,10 @@ type DBConfiguration struct { MigrationsPath string `json:"migrations_path" split_words:"true" default:"./migrations"` } +func (c *DBConfiguration) Validate() error { + return nil +} + // JWTConfiguration holds all the JWT related configuration. type JWTConfiguration struct { Secret string `json:"secret" required:"true"` @@ -52,15 +57,30 @@ type MFAConfiguration struct { ChallengeExpiryDuration float64 `json:"challenge_expiry_duration" default:"300" split_words:"true"` } +type APIConfiguration struct { + Host string + Port int `envconfig:"PORT" default:"8081"` + Endpoint string + RequestIDHeader string `envconfig:"REQUEST_ID_HEADER"` + ExternalURL string `json:"external_url" envconfig:"API_EXTERNAL_URL"` +} + +func (a *APIConfiguration) Validate() error { + if a.ExternalURL != "" { + // sometimes, in tests, ExternalURL is empty and we regard that + // as a valid value + _, err := url.ParseRequestURI(a.ExternalURL) + if err != nil { + return err + } + } + + return nil +} + // GlobalConfiguration holds all the configuration that applies to all instances. type GlobalConfiguration struct { - API struct { - Host string - Port int `envconfig:"PORT" default:"8081"` - Endpoint string - RequestIDHeader string `envconfig:"REQUEST_ID_HEADER"` - ExternalURL string `json:"external_url" envconfig:"API_EXTERNAL_URL"` - } + API APIConfiguration DB DBConfiguration External ProviderConfiguration Logging LoggingConfig `envconfig:"LOG"` @@ -134,6 +154,10 @@ type SMTPConfiguration struct { SenderName string `json:"sender_name" split_words:"true"` } +func (c *SMTPConfiguration) Validate() error { + return nil +} + type MailerConfiguration struct { Autoconfirm bool `json:"autoconfirm"` Subjects EmailContentConfiguration `json:"subjects"` @@ -239,7 +263,13 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) { return nil, err } - config.ApplyDefaults() + if err := config.ApplyDefaults(); err != nil { + return nil, err + } + + if err := config.Validate(); err != nil { + return nil, err + } if _, err := ConfigureLogging(&config.Logging); err != nil { return nil, err @@ -247,14 +277,11 @@ func LoadGlobal(filename string) (*GlobalConfiguration, error) { ConfigureTracing(&config.Tracing) - if config.SMTP.MaxFrequency == 0 { - config.SMTP.MaxFrequency = 1 * time.Minute - } return config, nil } // ApplyDefaults sets defaults for a GlobalConfiguration -func (config *GlobalConfiguration) ApplyDefaults() { +func (config *GlobalConfiguration) ApplyDefaults() error { if config.JWT.AdminGroupName == "" { config.JWT.AdminGroupName = "admin" } @@ -328,6 +355,7 @@ func (config *GlobalConfiguration) ApplyDefaults() { if config.URIAllowList == nil { config.URIAllowList = []string{} } + if config.URIAllowList != nil { config.URIAllowListMap = make(map[string]glob.Glob) for _, uri := range config.URIAllowList { @@ -335,12 +363,35 @@ func (config *GlobalConfiguration) ApplyDefaults() { config.URIAllowListMap[uri] = g } } + if config.PasswordMinLength < defaultMinPasswordLength { config.PasswordMinLength = defaultMinPasswordLength } if config.MFA.ChallengeExpiryDuration < defaultChallengeExpiryDuration { config.MFA.ChallengeExpiryDuration = defaultChallengeExpiryDuration } + + return nil +} + +// Validate validates all of configuration. +func (c *GlobalConfiguration) Validate() error { + validatables := []interface { + Validate() error + }{ + &c.API, + &c.DB, + &c.Tracing, + &c.SMTP, + } + + for _, validatable := range validatables { + if err := validatable.Validate(); err != nil { + return err + } + } + + return nil } func (o *OAuthProviderConfiguration) Validate() error { diff --git a/conf/tracing.go b/conf/tracing.go index 8f51ffe95..ad01540dc 100644 --- a/conf/tracing.go +++ b/conf/tracing.go @@ -16,6 +16,10 @@ type TracingConfig struct { Tags map[string]string } +func (tc *TracingConfig) Validate() error { + return nil +} + func (tc *TracingConfig) tracingAddr() string { return fmt.Sprintf("%s:%s", tc.Host, tc.Port) } diff --git a/crypto/crypto.go b/crypto/crypto.go index b3a617c48..70980c16c 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -37,22 +37,3 @@ func GenerateOtp(digits int) (string, error) { otp := fmt.Sprintf(expr, val.String()) return otp, nil } - -// GenerateOtpFromCharset generates a random n-length otp from a charset -func GenerateOtpFromCharset(length int, charset string) (string, error) { - b := make([]byte, length) - for i := range b { - val, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset)))) - if err != nil { - return "", errors.WithMessage(err, "Error generating otp from charset") - } - b[i] = charset[val.Int64()] - } - return string(b), nil -} - -// GenerateEmailOtp generates a random n-length alphanumeric otp -func GenerateEmailOtp(length int) (string, error) { - const charset = "abcdefghijklmnopqrstuvwxyz" - return GenerateOtpFromCharset(length, charset) -} diff --git a/logger/log.go b/logger/log.go index 8f5c14a98..705acfb4d 100644 --- a/logger/log.go +++ b/logger/log.go @@ -6,6 +6,7 @@ import ( "time" chimiddleware "github.com/go-chi/chi/middleware" + "github.com/netlify/gotrue/utilities" "github.com/sirupsen/logrus" ) @@ -26,7 +27,7 @@ func (l *structuredLogger) NewLogEntry(r *http.Request) chimiddleware.LogEntry { "component": "api", "method": r.Method, "path": r.URL.Path, - "remote_addr": r.RemoteAddr, + "remote_addr": utilities.GetIPAddress(r), "referer": r.Referer(), "timestamp": time.Now().UTC().Format(time.RFC3339), } diff --git a/models/connection.go b/models/connection.go index ddb50b872..446c58ff7 100644 --- a/models/connection.go +++ b/models/connection.go @@ -30,17 +30,24 @@ type SortField struct { Dir SortDirection } +// TruncateAll deletes all data from the database, as managed by GoTrue. Not +// intended for use outside of tests. func TruncateAll(conn *storage.Connection) error { return conn.Transaction(func(tx *storage.Connection) error { - if err := tx.RawQuery("delete from " + (&pop.Model{Value: User{}}).TableName()).Exec(); err != nil { - return err + tables := []string{ + (&pop.Model{Value: User{}}).TableName(), + (&pop.Model{Value: Identity{}}).TableName(), + (&pop.Model{Value: RefreshToken{}}).TableName(), + (&pop.Model{Value: AuditLogEntry{}}).TableName(), + (&pop.Model{Value: Session{}}).TableName(), } - if err := tx.RawQuery("delete from " + (&pop.Model{Value: RefreshToken{}}).TableName()).Exec(); err != nil { - return err - } - if err := tx.RawQuery("delete from " + (&pop.Model{Value: AuditLogEntry{}}).TableName()).Exec(); err != nil { - return err + + for _, tableName := range tables { + if err := tx.RawQuery("TRUNCATE " + tableName + " CASCADE").Exec(); err != nil { + return err + } } + return nil }) } diff --git a/models/instance.go b/models/instance.go deleted file mode 100644 index ef85a8b1e..000000000 --- a/models/instance.go +++ /dev/null @@ -1,87 +0,0 @@ -package models - -import ( - "database/sql" - "time" - - "github.com/gobuffalo/pop/v5" - "github.com/gofrs/uuid" - "github.com/netlify/gotrue/conf" - "github.com/netlify/gotrue/storage" - "github.com/pkg/errors" -) - -type Instance struct { - ID uuid.UUID `json:"id" db:"id"` - // Netlify UUID - UUID uuid.UUID `json:"uuid,omitempty" db:"uuid"` - - BaseConfig *conf.GlobalConfiguration `json:"config" db:"raw_base_config"` - - CreatedAt time.Time `json:"created_at" db:"created_at"` - UpdatedAt time.Time `json:"updated_at" db:"updated_at"` -} - -func (Instance) TableName() string { - tableName := "instances" - return tableName -} - -// Config loads the the base configuration values with defaults. -func (i *Instance) Config() (*conf.GlobalConfiguration, error) { - if i.BaseConfig == nil { - return nil, errors.New("no configuration data available") - } - - baseConf := &conf.GlobalConfiguration{} - *baseConf = *i.BaseConfig - baseConf.ApplyDefaults() - - return baseConf, nil -} - -// UpdateConfig updates the base config -func (i *Instance) UpdateConfig(tx *storage.Connection, config *conf.GlobalConfiguration) error { - i.BaseConfig = config - return tx.UpdateOnly(i, "raw_base_config") -} - -// GetInstance finds an instance by ID -func GetInstance(tx *storage.Connection, instanceID uuid.UUID) (*Instance, error) { - instance := Instance{} - if err := tx.Find(&instance, instanceID); err != nil { - if errors.Cause(err) == sql.ErrNoRows { - return nil, InstanceNotFoundError{} - } - return nil, errors.Wrap(err, "error finding instance") - } - return &instance, nil -} - -func GetInstanceByUUID(tx *storage.Connection, uuid uuid.UUID) (*Instance, error) { - instance := Instance{} - if err := tx.Where("uuid = ?", uuid).First(&instance); err != nil { - if errors.Cause(err) == sql.ErrNoRows { - return nil, InstanceNotFoundError{} - } - return nil, errors.Wrap(err, "error finding instance") - } - return &instance, nil -} - -func DeleteInstance(conn *storage.Connection, instance *Instance) error { - return conn.Transaction(func(tx *storage.Connection) error { - delModels := map[string]*pop.Model{ - "user": {Value: &User{}}, - "refresh token": {Value: &RefreshToken{}}, - } - - for name, dm := range delModels { - if err := tx.RawQuery("DELETE FROM "+dm.TableName()+" WHERE instance_id = ?", instance.ID).Exec(); err != nil { - return errors.Wrapf(err, "Error deleting %s records", name) - } - } - - return errors.Wrap(tx.Destroy(instance), "Error deleting instance record") - }) -} diff --git a/models/refresh_token.go b/models/refresh_token.go index 5f1bc4630..eb2ab3d51 100644 --- a/models/refresh_token.go +++ b/models/refresh_token.go @@ -36,9 +36,14 @@ func (RefreshToken) TableName() string { return tableName } +// GrantParams is used to pass session-specific parameters when issuing a new +// refresh token to authenticated users. +type GrantParams struct { +} + // GrantAuthenticatedUser creates a refresh token for the provided user. -func GrantAuthenticatedUser(tx *storage.Connection, user *User) (*RefreshToken, error) { - return createRefreshToken(tx, user, nil) +func GrantAuthenticatedUser(tx *storage.Connection, user *User, params GrantParams) (*RefreshToken, error) { + return createRefreshToken(tx, user, nil, ¶ms) } // GrantRefreshTokenSwap swaps a refresh token for a new one, revoking the provided token. @@ -54,7 +59,7 @@ func GrantRefreshTokenSwap(r *http.Request, tx *storage.Connection, user *User, if terr = tx.UpdateOnly(token, "revoked"); terr != nil { return terr } - newToken, terr = createRefreshToken(rtx, user, token) + newToken, terr = createRefreshToken(rtx, user, token, nil) return terr }) return newToken, err @@ -94,7 +99,7 @@ func GetValidChildToken(tx *storage.Connection, token *RefreshToken) (*RefreshTo return refreshToken, nil } -func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshToken) (*RefreshToken, error) { +func createRefreshToken(tx *storage.Connection, user *User, oldToken *RefreshToken, params *GrantParams) (*RefreshToken, error) { token := &RefreshToken{ UserID: user.ID, Token: crypto.SecureToken(), diff --git a/models/refresh_token_test.go b/models/refresh_token_test.go index cbf32af25..6da738ba5 100644 --- a/models/refresh_token_test.go +++ b/models/refresh_token_test.go @@ -37,7 +37,7 @@ func TestRefreshToken(t *testing.T) { func (ts *RefreshTokenTestSuite) TestGrantAuthenticatedUser() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{}) require.NoError(ts.T(), err) require.NotEmpty(ts.T(), r.Token) @@ -46,7 +46,7 @@ func (ts *RefreshTokenTestSuite) TestGrantAuthenticatedUser() { func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{}) require.NoError(ts.T(), err) s, err := GrantRefreshTokenSwap(&http.Request{}, ts.db, u, r) @@ -64,7 +64,7 @@ func (ts *RefreshTokenTestSuite) TestGrantRefreshTokenSwap() { func (ts *RefreshTokenTestSuite) TestLogout() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{}) require.NoError(ts.T(), err) require.NoError(ts.T(), Logout(ts.db, u.ID)) diff --git a/models/user_test.go b/models/user_test.go index 8ff308a49..eb75a599a 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -148,7 +148,7 @@ func (ts *UserTestSuite) TestFindUserByRecoveryToken() { func (ts *UserTestSuite) TestFindUserWithRefreshToken() { u := ts.createUser() - r, err := GrantAuthenticatedUser(ts.db, u) + r, err := GrantAuthenticatedUser(ts.db, u, GrantParams{}) require.NoError(ts.T(), err) n, nr, err := FindUserWithRefreshToken(ts.db, r.Token) diff --git a/security/hcaptcha.go b/security/hcaptcha.go index c584f77ec..a0014ab19 100644 --- a/security/hcaptcha.go +++ b/security/hcaptcha.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/netlify/gotrue/utilities" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) @@ -77,7 +78,7 @@ func VerifyRequest(r *http.Request, secretKey string) (VerificationResult, error if err != nil || strings.TrimSpace(res.Security.Token) == "" { return UserRequestFailed, errors.Wrap(err, "couldn't decode captcha info") } - clientIP := strings.Split(r.RemoteAddr, ":")[0] + clientIP := utilities.GetIPAddress(r) return verifyCaptchaCode(res.Security.Token, secretKey, clientIP) } diff --git a/utilities/request.go b/utilities/request.go new file mode 100644 index 000000000..3e244b7ba --- /dev/null +++ b/utilities/request.go @@ -0,0 +1,35 @@ +package utilities + +import ( + "net" + "net/http" + "strings" +) + +// GetIPAddress returns the real IP address of the HTTP request. It parses the +// X-Forwarded-For header. +func GetIPAddress(r *http.Request) string { + if r.Header != nil { + xForwardedFor := r.Header.Get("X-Forwarded-For") + if xForwardedFor != "" { + ips := strings.Split(xForwardedFor, ",") + for i := range ips { + ips[i] = strings.TrimSpace(ips[i]) + } + + for _, ip := range ips { + if ip != "" { + return ip + } + } + } + } + + ipPort := r.RemoteAddr + ip, _, err := net.SplitHostPort(ipPort) + if err != nil { + return ipPort + } + + return ip +} diff --git a/utilities/request_test.go b/utilities/request_test.go new file mode 100644 index 000000000..da76042e0 --- /dev/null +++ b/utilities/request_test.go @@ -0,0 +1,87 @@ +package utilities + +import ( + "net/http" + tst "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetIPAddress(t *tst.T) { + examples := []func(r *http.Request) string{ + func(r *http.Request) string { + r.Header = nil + r.RemoteAddr = "127.0.0.1:8080" + + return "127.0.0.1" + }, + + func(r *http.Request) string { + r.Header = nil + r.RemoteAddr = "incorrect" + + return "incorrect" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "127.0.0.1:8080" + + return "127.0.0.1" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "[::1]:8080" + + return "::1" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "127.0.0.1:8080" + r.Header.Add("X-Forwarded-For", "127.0.0.2") + + return "127.0.0.2" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "127.0.0.1:8080" + r.Header.Add("X-Forwarded-For", "127.0.0.2") + + return "127.0.0.2" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "127.0.0.1:8080" + r.Header.Add("X-Forwarded-For", "127.0.0.2,") + + return "127.0.0.2" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "127.0.0.1:8080" + r.Header.Add("X-Forwarded-For", "127.0.0.2,127.0.0.3") + + return "127.0.0.2" + }, + + func(r *http.Request) string { + r.Header = make(http.Header) + r.RemoteAddr = "127.0.0.1:8080" + r.Header.Add("X-Forwarded-For", "::1,127.0.0.2") + + return "::1" + }, + } + + for _, example := range examples { + req := &http.Request{} + expected := example(req) + + require.Equal(t, GetIPAddress(req), expected) + } +} From 14738e38892634c6946ffaa68850e9516a6dd522 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 2 Sep 2022 10:34:53 +0530 Subject: [PATCH 123/180] tests: add notion of first log in --- api/mfa.go | 24 ++++++++++++++++++------ api/mfa_test.go | 49 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 1e10d4c57..2e0a57e67 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -48,6 +48,13 @@ type ChallengeFactorResponse struct { type VerifyFactorResponse struct { Success string `json:"success"` } +type StepUpLoginCodeResponse struct { + Success string `json:"success"` +} + +type StepUpLoginRecoveryCodeResponse struct { + RecoveryCodes []*models.RecoveryCode `json:"recovery_codes"` +} type UnenrollFactorResponse struct { Success string `json:"success"` @@ -187,6 +194,10 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { if params.Code != "" { // TODO(suggest): Either reorganize to token grant style case statement with types OR dump this into models + valid := totp.Validate(params.Code, factor.SecretKey) + if !valid { + return unauthorizedError("Invalid code entered") + } challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) if err != nil { if models.IsNotFoundError(err) { @@ -209,10 +220,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { return expiredChallengeError("%v has expired, please verify against another challenge or create a new challenge.", challenge.ID) } - valid := totp.Validate(params.Code, factor.SecretKey) - if !valid { - return unauthorizedError("Invalid code entered") - } + actionType = string(models.MFACodeLoginAction) } else if params.RecoveryCode != "" { err := a.db.Transaction(func(tx *storage.Connection) error { @@ -271,10 +279,14 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { // TODO(Joel): Find a way to refactor this in a transaction if shouldReturnRecoveryCodes { - return sendJSON(w, http.StatusOK, recoveryCodes) + return sendJSON(w, http.StatusOK, &StepUpLoginRecoveryCodeResponse{ + RecoveryCodes: recoveryCodes, + }) } - return sendJSON(w, http.StatusOK, token) + return sendJSON(w, http.StatusOK, &StepUpLoginCodeResponse{ + Success: fmt.Sprintf("true"), + }) } func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { diff --git a/api/mfa_test.go b/api/mfa_test.go index c68c71d07..00645e19c 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -321,22 +321,14 @@ func (ts *MFATestSuite) TestStepUpLogin() { true, http.StatusUnprocessableEntity, }, - // { - // "Should not return recovery codes if not first login", - // false, - // true, - // false, - // true, - // http.StatusOK, - // }, - // { - // "Successful login with recovery code", - // false, - // true, - // false, - // true, - // http.StatusOK, - // }, + { + "Successful login with recovery code", + false, + true, + false, + true, + http.StatusOK, + }, // { // "Login without 1FA verified should fail", // "", @@ -381,6 +373,7 @@ func (ts *MFATestSuite) TestStepUpLogin() { code := "" if v.IsRecoveryCodeLogin { + // Need to create actual recovery Codes to handle recoveryCode = "123456" } if v.IsCodeLogin { @@ -393,12 +386,34 @@ func (ts *MFATestSuite) TestStepUpLogin() { "code": code, "recovery_code": recoveryCode, })) + if !v.IsFirstMFALogin { + t := time.Now() + u.RecoveryCodesReceivedAt = &t + require.NoError(ts.T(), ts.API.db.Update(u), "Error updating user") + + } req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/%s/login", u.ID, f.ID), &buffer) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) ts.API.handler.ServeHTTP(w, req) require.Equal(ts.T(), v.ExpectedHTTPCode, w.Code) - // Assertion logic + // Check for first recovery code login + if v.ExpectedHTTPCode == http.StatusOK && v.IsFirstMFALogin && v.IsCodeLogin { + data := StepUpLoginRecoveryCodeResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.NotEmpty(ts.T(), data.RecoveryCodes) + } else if v.ExpectedHTTPCode == http.StatusOK && v.IsCodeLogin && !v.IsFirstMFALogin { + data := StepUpLoginCodeResponse{} + require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + require.Equal(ts.T(), "true", data.Success) + } + + // else if v.ExpectedHTTPCode == http.StatusOK && v.IsRecoveryCodeLogin { + // data := StepUpLoginCodeResponse{} + // require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) + // require.Equal(ts.T(), "true", data.Success) + + // } }) } From 54022210ad75576f467371a38714a877f0ae3865 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Fri, 2 Sep 2022 11:50:52 +0530 Subject: [PATCH 124/180] feat: add session logic --- api/mfa.go | 2 ++ api/mfa_test.go | 2 ++ api/token.go | 34 +++++++++++++++------------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/api/mfa.go b/api/mfa.go index 2e0a57e67..d8d47e6a6 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -49,6 +49,7 @@ type VerifyFactorResponse struct { Success string `json:"success"` } type StepUpLoginCodeResponse struct { + Token string `json:"token"` Success string `json:"success"` } @@ -285,6 +286,7 @@ func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { } return sendJSON(w, http.StatusOK, &StepUpLoginCodeResponse{ + Token: token.Token, Success: fmt.Sprintf("true"), }) } diff --git a/api/mfa_test.go b/api/mfa_test.go index 00645e19c..3877b0ba4 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -406,6 +406,8 @@ func (ts *MFATestSuite) TestStepUpLogin() { data := StepUpLoginCodeResponse{} require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) require.Equal(ts.T(), "true", data.Success) + // TODO(Joel): Parse this and show that the claims are valid + // require.Nil(ts.T(), data.Token) } // else if v.ExpectedHTTPCode == http.StatusOK && v.IsRecoveryCodeLogin { diff --git a/api/token.go b/api/token.go index 1a16a22df..d65c8b1ec 100644 --- a/api/token.go +++ b/api/token.go @@ -566,7 +566,7 @@ func generateAccessToken(user *models.User, sessionId string, expiresIn time.Dur func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, user *models.User, signInMethod, factorID string) (*AccessTokenResponse, error) { config := a.config - // isMFASignInMethod := signInMethod == models.TOTP + isMFASignInMethod := signInMethod == models.TOTP now := time.Now() user.LastSignInAt = &now @@ -582,24 +582,20 @@ func (a *API) issueRefreshToken(ctx context.Context, conn *storage.Connection, u return internalServerError("Database error granting user").WithInternalError(terr) } - // session := getSession(ctx) - // if session == nil { - // err, session := models.FindSessionById(tx, refreshToken.SessionId) - // if err != nil { - // return err - // } - // } - // // What happens if session is nil - // terr = models.AddClaimToSession(tx, session, signInMethod) - // if terr != nil { - // return terr - // } - - // if isMFASignInMethod { - // if err := session.UpdateAssociatedFactor(tx, factorID); err != nil { - // return err - // } - // } + session, terr := models.FindSessionById(tx, refreshToken.SessionId.UUID) + if terr != nil { + return terr + } + terr = models.AddClaimToSession(tx, session, signInMethod) + if terr != nil { + return terr + } + + if isMFASignInMethod { + if err := session.UpdateAssociatedFactor(tx, factorID); err != nil { + return err + } + } tokenString, terr = generateAccessToken(user, refreshToken.SessionId.UUID.String(), time.Second*time.Duration(config.JWT.Exp), config.JWT.Secret, currentClaims, signInMethod) if terr != nil { From aba96e48e2c65aa43a5a4cb2d944ca2b20ff1e37 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Mon, 5 Sep 2022 10:23:38 +0300 Subject: [PATCH 125/180] refactor: remove stepup login --- api/api.go | 5 -- api/mfa.go | 129 +++----------------------------------- api/mfa_test.go | 125 ------------------------------------ models/audit_log_entry.go | 2 - 4 files changed, 9 insertions(+), 252 deletions(-) diff --git a/api/api.go b/api/api.go index 4288a62cc..0ddec44db 100644 --- a/api/api.go +++ b/api/api.go @@ -170,11 +170,6 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Post("/challenge", api.ChallengeFactor) r.Delete("/", api.UnenrollFactor) - r.Route("/login", func(r *router) { - r.Use(api.require1FA) - r.Post("/", api.StepUpLogin) - }) - }) }) }) diff --git a/api/mfa.go b/api/mfa.go index d8d47e6a6..a53fae606 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -171,125 +171,6 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { }) } -// TODO(Joel): Move over other supporting changes from other branch. Don't use until properly tested. -func (a *API) StepUpLogin(w http.ResponseWriter, r *http.Request) error { - ctx := r.Context() - config := a.config - user := getUser(ctx) - factor := getFactor(ctx) - - if factor.Status != models.FactorVerifiedState { - return unprocessableEntityError("Please attempt a login with a verified factor") - } - actionType := "" - - params := &StepUpLoginParams{} - jsonDecoder := json.NewDecoder(r.Body) - err := jsonDecoder.Decode(params) - if err != nil { - return badRequestError("Please check the params passed into StepupLogin: %v", err) - } - if params.Code != "" && params.RecoveryCode != "" { - return unprocessableEntityError("Please attempt a login with only one of Code or Recovery Code'") - } - - if params.Code != "" { - // TODO(suggest): Either reorganize to token grant style case statement with types OR dump this into models - valid := totp.Validate(params.Code, factor.SecretKey) - if !valid { - return unauthorizedError("Invalid code entered") - } - challenge, err := models.FindChallengeByChallengeID(a.db, params.ChallengeID) - if err != nil { - if models.IsNotFoundError(err) { - return internalServerError(err.Error()) - } - return internalServerError("Database error finding Challenge").WithInternalError(err) - } - hasExpired := time.Now().After(challenge.CreatedAt.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration))) - if hasExpired { - err := a.db.Transaction(func(tx *storage.Connection) error { - if terr := tx.Destroy(challenge); terr != nil { - return internalServerError("Database error deleting challenge").WithInternalError(terr) - } - - return nil - }) - if err != nil { - return err - } - - return expiredChallengeError("%v has expired, please verify against another challenge or create a new challenge.", challenge.ID) - } - - actionType = string(models.MFACodeLoginAction) - } else if params.RecoveryCode != "" { - err := a.db.Transaction(func(tx *storage.Connection) error { - rc, terr := models.IsRecoveryCodeValid(tx, user, params.RecoveryCode) - if terr != nil { - return terr - } - if rc.RecoveryCode == params.RecoveryCode { - terr = rc.Consume(tx) - if terr != nil { - return terr - } - } else { - return unauthorizedError("Invalid code entered") - } - - return nil - - }) - if err != nil { - return err - } - actionType = string(models.MFARecoveryCodeLoginAction) - - } - var token *AccessTokenResponse - var recoveryCodes []*models.RecoveryCode - - var terr error - shouldReturnRecoveryCodes := !user.HasReceivedRecoveryCodes() && actionType != string(models.MFARecoveryCodeLoginAction) - - terr = a.db.Transaction(func(tx *storage.Connection) error { - if terr := models.NewAuditLogEntry(r, tx, user, models.AuditAction(actionType), r.RemoteAddr, nil); terr != nil { - return terr - } - - token, terr = a.issueRefreshToken(ctx, tx, user, models.TOTP, factor.ID) - if terr != nil { - return terr - } - - if terr = a.setCookieTokens(config, token, false, w); terr != nil { - return internalServerError("Failed to set JWT cookie. %s", terr) - } - recoveryCodes, err = models.GenerateBatchOfRecoveryCodes(tx, user) - if err != nil { - return err - } - - return nil - }) - if err != nil { - return err - } - metering.RecordLogin(actionType, user.ID) - - // TODO(Joel): Find a way to refactor this in a transaction - if shouldReturnRecoveryCodes { - return sendJSON(w, http.StatusOK, &StepUpLoginRecoveryCodeResponse{ - RecoveryCodes: recoveryCodes, - }) - } - - return sendJSON(w, http.StatusOK, &StepUpLoginCodeResponse{ - Token: token.Token, - Success: fmt.Sprintf("true"), - }) -} func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { var err error @@ -353,12 +234,20 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return err } } + token, terr := a.issueRefreshToken(ctx, tx, user, models.TOTP, factor.ID) + if terr != nil { + return terr + } + if terr = a.setCookieTokens(config, token, false, w); terr != nil { + return internalServerError("Failed to set JWT cookie. %s", terr) + } return nil }) if err != nil { return err } - // InvalidateSessionWith Exxception + metering.RecordLogin(string(models.MFACodeLoginAction), user.ID) + // TODO(Joel): Invalidate all sessions with <= AAL1 return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ Success: fmt.Sprintf("%v", valid), diff --git a/api/mfa_test.go b/api/mfa_test.go index 3877b0ba4..978fb67e9 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -295,128 +295,3 @@ func (ts *MFATestSuite) TestUnenrollFactor() { } } - -func (ts *MFATestSuite) TestStepUpLogin() { - cases := []struct { - desc string - IsRecoveryCodeLogin bool - IsCodeLogin bool - IsFirstMFALogin bool - IsOneFAVerified bool - ExpectedHTTPCode int - }{ - { - "Successful login with code", - false, - true, - true, - true, - http.StatusOK, - }, - - {"Using both code and recovery code is forbidden", - true, - true, - true, - true, - http.StatusUnprocessableEntity, - }, - { - "Successful login with recovery code", - false, - true, - false, - true, - http.StatusOK, - }, - // { - // "Login without 1FA verified should fail", - // "", - // true, - // false, - // http.StatusForbidden, - // }, - } - for _, v := range cases { - ts.Run(v.desc, func() { - var buffer bytes.Buffer - u, err := models.FindUserByEmailAndAudience(ts.API.db, "test@example.com", ts.Config.JWT.Aud) - emailValue, err := u.Email.Value() - require.NoError(ts.T(), err) - testEmail := emailValue.(string) - testDomain := strings.Split(testEmail, "@")[1] - // Set factor secret - key, err := totp.Generate(totp.GenerateOpts{ - Issuer: testDomain, - AccountName: testEmail, - }) - sharedSecret := key.Secret() - - factors, err := models.FindFactorsByUser(ts.API.db, u) - f := factors[0] - f.SecretKey = sharedSecret - require.NoError(ts.T(), ts.API.db.Update(f), "Error updating new test factor") - - err = f.UpdateStatus(ts.API.db, models.FactorVerifiedState) - require.NoError(ts.T(), err) - - c, err := models.NewChallenge(f) - require.NoError(ts.T(), err, "Error creating test Challenge model") - require.NoError(ts.T(), ts.API.db.Create(c), "Error saving new test challenge") - // Sign in with regular email password - token, err := generateAccessToken(u, "", time.Second*time.Duration(ts.Config.JWT.Exp), ts.Config.JWT.Secret, nil, "") - require.NoError(ts.T(), err) - w := httptest.NewRecorder() - - //TODO(Joel): Patch this portion to properly check all cases surrounding login - recoveryCode := "" - code := "" - - if v.IsRecoveryCodeLogin { - // Need to create actual recovery Codes to handle - recoveryCode = "123456" - } - if v.IsCodeLogin { - code, err = totp.GenerateCode(sharedSecret, time.Now().UTC()) - require.NoError(ts.T(), err) - } - - require.NoError(ts.T(), json.NewEncoder(&buffer).Encode(map[string]interface{}{ - "challenge_id": c.ID, - "code": code, - "recovery_code": recoveryCode, - })) - if !v.IsFirstMFALogin { - t := time.Now() - u.RecoveryCodesReceivedAt = &t - require.NoError(ts.T(), ts.API.db.Update(u), "Error updating user") - - } - - req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/user/%s/factor/%s/login", u.ID, f.ID), &buffer) - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - ts.API.handler.ServeHTTP(w, req) - require.Equal(ts.T(), v.ExpectedHTTPCode, w.Code) - // Check for first recovery code login - if v.ExpectedHTTPCode == http.StatusOK && v.IsFirstMFALogin && v.IsCodeLogin { - data := StepUpLoginRecoveryCodeResponse{} - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.NotEmpty(ts.T(), data.RecoveryCodes) - } else if v.ExpectedHTTPCode == http.StatusOK && v.IsCodeLogin && !v.IsFirstMFALogin { - data := StepUpLoginCodeResponse{} - require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - require.Equal(ts.T(), "true", data.Success) - // TODO(Joel): Parse this and show that the claims are valid - // require.Nil(ts.T(), data.Token) - } - - // else if v.ExpectedHTTPCode == http.StatusOK && v.IsRecoveryCodeLogin { - // data := StepUpLoginCodeResponse{} - // require.NoError(ts.T(), json.NewDecoder(w.Body).Decode(&data)) - // require.Equal(ts.T(), "true", data.Success) - - // } - }) - } - -} diff --git a/models/audit_log_entry.go b/models/audit_log_entry.go index 2005076e4..aa20327f6 100644 --- a/models/audit_log_entry.go +++ b/models/audit_log_entry.go @@ -39,7 +39,6 @@ const ( DeleteRecoveryCodesAction AuditAction = "recovery_codes_deleted" UpdateFactorAction AuditAction = "factor_updated" MFACodeLoginAction AuditAction = "mfa_code_login" - MFARecoveryCodeLoginAction AuditAction = "mfa_recovery_code_login" account auditLogType = "account" team auditLogType = "team" @@ -70,7 +69,6 @@ var ActionLogTypeMap = map[AuditAction]auditLogType{ DeleteFactorAction: factor, UpdateFactorAction: factor, MFACodeLoginAction: factor, - MFARecoveryCodeLoginAction: recoveryCodes, DeleteRecoveryCodesAction: recoveryCodes, } From ea783cc0ca3ea9269fa48bffd472ed81f2dc7676 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 6 Sep 2022 15:06:30 +0300 Subject: [PATCH 126/180] feat: remove stepup login related details --- api/api.go | 6 ++- api/mfa.go | 41 +++++++++---------- api/mfa_test.go | 2 + api/token.go | 1 - .../20220830041349_add_mfa_schema.up.sql | 3 +- models/sessions.go | 10 +++-- models/user.go | 6 --- 7 files changed, 33 insertions(+), 36 deletions(-) diff --git a/api/api.go b/api/api.go index 0ddec44db..b77e2d88c 100644 --- a/api/api.go +++ b/api/api.go @@ -164,8 +164,9 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati r.Use(api.loadUser) r.Route("/factor", func(r *router) { r.Post("/", api.EnrollFactor) - r.Route("/{factor_id}", func(r *router) { + r.With(api.requireAuthentication).Route("/{factor_id}", func(r *router) { r.Use(api.loadFactor) + r.Post("/verify", api.VerifyFactor) r.Post("/challenge", api.ChallengeFactor) r.Delete("/", api.UnenrollFactor) @@ -212,8 +213,9 @@ func NewAPIWithVersion(ctx context.Context, globalConfig *conf.GlobalConfigurati corsHandler := cors.New(cors.Options{ AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", audHeaderName, useCookieHeader}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Client-IP", "X-Client-Info", audHeaderName, useCookieHeader}, AllowCredentials: true, + Debug: true, }) api.handler = corsHandler.Handler(chi.ServerBaseContext(ctx, r)) diff --git a/api/mfa.go b/api/mfa.go index a53fae606..752e0e16e 100644 --- a/api/mfa.go +++ b/api/mfa.go @@ -48,14 +48,6 @@ type ChallengeFactorResponse struct { type VerifyFactorResponse struct { Success string `json:"success"` } -type StepUpLoginCodeResponse struct { - Token string `json:"token"` - Success string `json:"success"` -} - -type StepUpLoginRecoveryCodeResponse struct { - RecoveryCodes []*models.RecoveryCode `json:"recovery_codes"` -} type UnenrollFactorResponse struct { Success string `json:"success"` @@ -65,16 +57,13 @@ type UnenrollFactorParams struct { Code string `json:"code"` } -type StepUpLoginParams struct { - ChallengeID string `json:"challenge_id"` - Code string `json:"code"` - RecoveryCode string `json:"recovery_code"` -} - func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { const factorPrefix = "factor" ctx := r.Context() user := getUser(ctx) + // if len(user.Factors) >=1 { + // // return badRequestError("Only one factor can be enrolled at a time, please unenroll to continue") + // } params := &EnrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -83,8 +72,8 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { return badRequestError(err.Error()) } factorType := params.FactorType - if (factorType != models.TOTP) && (factorType != models.Webauthn) { - return unprocessableEntityError("FactorType needs to be either 'totp' or 'webauthn'") + if factorType != models.TOTP { + return unprocessableEntityError("FactorType needs to be TOTP") } if params.Issuer == "" { return unprocessableEntityError("Issuer is required") @@ -125,6 +114,7 @@ func (a *API) EnrollFactor(w http.ResponseWriter, r *http.Request) error { if terr != nil { return terr } + // TODO(Joel):Escape the characters accordingly so that it can be copied return sendJSON(w, http.StatusOK, &EnrollFactorResponse{ ID: factor.ID, Type: factor.FactorType, @@ -165,13 +155,13 @@ func (a *API) ChallengeFactor(w http.ResponseWriter, r *http.Request) error { creationTime := challenge.CreatedAt expiryTime := creationTime.Add(time.Second * time.Duration(config.MFA.ChallengeExpiryDuration)) + // TODO(Joel): Convert this to unix timestamp return sendJSON(w, http.StatusOK, &ChallengeFactorResponse{ ID: challenge.ID, - ExpiresAt: expiryTime.String(), + ExpiresAt: fmt.Sprintf("%v", expiryTime.Unix()), }) } - func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { var err error ctx := r.Context() @@ -218,7 +208,6 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { return expiredChallengeError("%v has expired, please verify against another challenge or create a new challenge.", challenge.ID) } - err = a.db.Transaction(func(tx *storage.Connection) error { if err = models.NewAuditLogEntry(r, tx, user, models.VerifyFactorAction, r.RemoteAddr, map[string]interface{}{ "factor_id": factor.ID, @@ -241,13 +230,15 @@ func (a *API) VerifyFactor(w http.ResponseWriter, r *http.Request) error { if terr = a.setCookieTokens(config, token, false, w); terr != nil { return internalServerError("Failed to set JWT cookie. %s", terr) } + if terr = models.InvalidateSessionsWithAALLessThan(tx, user.ID, 2); terr != nil { + return internalServerError("Failed to update sessions. %s", terr) + } return nil }) if err != nil { return err } metering.RecordLogin(string(models.MFACodeLoginAction), user.ID) - // TODO(Joel): Invalidate all sessions with <= AAL1 return sendJSON(w, http.StatusOK, &VerifyFactorResponse{ Success: fmt.Sprintf("%v", valid), @@ -260,6 +251,7 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { ctx := r.Context() user := getUser(ctx) factor := getFactor(ctx) + session := getSession(ctx) params := &UnenrollFactorParams{} jsonDecoder := json.NewDecoder(r.Body) @@ -273,6 +265,9 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { } else if !MFAEnabled { return forbiddenError("You do not have a verified factor enrolled") } + if session == nil { + return badRequestError("session is not available") + } valid := totp.Validate(params.Code, factor.SecretKey) if valid != true { @@ -289,14 +284,16 @@ func (a *API) UnenrollFactor(w http.ResponseWriter, r *http.Request) error { }); err != nil { return err } + // TODO (Joel)Write test for this + if err = models.InvalidateOtherFactorAssociatedSessions(tx, session.ID, user.ID, factor.ID); err != nil { + return err + } return nil }) if err != nil { return err } - // Drop all Sessions to AAL1 here - return sendJSON(w, http.StatusOK, &UnenrollFactorResponse{ Success: fmt.Sprintf("%v", valid), }) diff --git a/api/mfa_test.go b/api/mfa_test.go index 978fb67e9..1b03ab8e2 100644 --- a/api/mfa_test.go +++ b/api/mfa_test.go @@ -134,6 +134,7 @@ func (ts *MFATestSuite) TestChallengeFactor() { require.Equal(ts.T(), http.StatusOK, w.Code) } +// TODO: Check behavior that downgrades all other sessions func (ts *MFATestSuite) TestMFAVerifyFactor() { cases := []struct { desc string @@ -225,6 +226,7 @@ func (ts *MFATestSuite) TestMFAVerifyFactor() { _, err := models.FindChallengeByChallengeID(ts.API.db, c.ID) require.EqualError(ts.T(), err, models.ChallengeNotFoundError{}.Error()) } + // Check the JWT to see if AAL is appropriate }) } } diff --git a/api/token.go b/api/token.go index d65c8b1ec..3f6dbb356 100644 --- a/api/token.go +++ b/api/token.go @@ -534,7 +534,6 @@ func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.R return sendJSON(w, http.StatusOK, token) } -// TODO(Joel): Add Factor ID to access token func generateAccessToken(user *models.User, sessionId string, expiresIn time.Duration, secret string, oldClaims *GoTrueClaims, signInMethod string) (string, error) { amr := []AMREntry{} aal := "aal1" diff --git a/migrations/20220830041349_add_mfa_schema.up.sql b/migrations/20220830041349_add_mfa_schema.up.sql index 6ae20a97d..9da467759 100644 --- a/migrations/20220830041349_add_mfa_schema.up.sql +++ b/migrations/20220830041349_add_mfa_schema.up.sql @@ -45,10 +45,9 @@ CREATE TABLE IF NOT EXISTS auth.mfa_recovery_codes( ); comment on table auth.mfa_recovery_codes is 'Auth: stores recovery codes for Multi Factor Authentication'; --- Add time at which recovery codes were issued -ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS recovery_codes_received_at timestamptz NULL; -- Add factor_id to sessions ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS factor_id text NULL; +ALTER TABLE auth.sessions ADD COLUMN IF NOT EXISTS aal integer NULL; -- Add factor_id and AMR claims to session CREATE TABLE IF NOT EXISTS auth.mfa_amr_claims( diff --git a/models/sessions.go b/models/sessions.go index 356fd67b1..2f36aa2ef 100644 --- a/models/sessions.go +++ b/models/sessions.go @@ -17,6 +17,7 @@ type Session struct { UpdatedAt time.Time `json:"updated_at" db:"updated_at"` FactorID string `json:"factor_id" db:"factor_id"` AMRClaims []AMRClaim `json:"amr_claims" has_many:"amr_claims"` + AAL int `json:"aal" db:"aal"` } func (Session) TableName() string { @@ -60,9 +61,12 @@ func FindSessionById(tx *storage.Connection, id uuid.UUID) (*Session, error) { return session, nil } -// TODO(Joel): Invalidate all other sessions once MFA is enabled ( A verified factor has been produced). Make use of this in unenroll -func InvalidateSessionsExcludingCurrent(tx *storage.Connection, currentSessionID uuid.UUID, userID uuid.UUID) error { - return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND session_id != ?", userID, currentSessionID).Exec() +func InvalidateOtherFactorAssociatedSessions(tx *storage.Connection, currentSessionID, userID uuid.UUID, factorID string) error { + return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND factor_id = ? AND session_id != ?", userID, factorID, currentSessionID).Exec() +} + +func InvalidateSessionsWithAALLessThan(tx *storage.Connection, userID uuid.UUID, level int) error { + return tx.RawQuery("DELETE FROM "+(&pop.Model{Value: Session{}}).TableName()+" WHERE user_id = ? AND aal <= ?", userID, level).Exec() } // Logout deletes all sessions for a user. diff --git a/models/user.go b/models/user.go index 5d4303e1c..84f303720 100644 --- a/models/user.go +++ b/models/user.go @@ -63,8 +63,6 @@ type User struct { UpdatedAt time.Time `json:"updated_at" db:"updated_at"` BannedUntil *time.Time `json:"banned_until,omitempty" db:"banned_until"` - RecoveryCodesReceivedAt *time.Time `json:"recovery_codes_recived_at" db:"recovery_codes_received_at"` - DONTUSEINSTANCEID uuid.UUID `json:"-" db:"instance_id"` } @@ -544,7 +542,3 @@ func (u *User) RemoveUnconfirmedIdentities(tx *storage.Connection) error { return nil } - -func (u *User) HasReceivedRecoveryCodes() bool { - return u.RecoveryCodesReceivedAt != nil -} From d00c49662a54d77836afa37f744fc4419f36c405 Mon Sep 17 00:00:00 2001 From: "joel@joellee.org" Date: Tue, 6 Sep 2022 20:35:58 +0300 Subject: [PATCH 127/180] refactor: remove unused fields --- api/debug.test | Bin 0 -> 26650834 bytes api/mfa.go | 42 +++++++++++++++++++++++++----------------- api/mfa_test.go | 30 ++++++++++++++++-------------- api/signup_test.go | 2 +- 4 files changed, 42 insertions(+), 32 deletions(-) create mode 100755 api/debug.test diff --git a/api/debug.test b/api/debug.test new file mode 100755 index 0000000000000000000000000000000000000000..d823c8a12b35160cb60b31dc24e6773e9655a6b0 GIT binary patch literal 26650834 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

io49WeWL-?Z}0`YAlQYW)1jx*z#r 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

TR-BCMhl9bki(OYl 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 z 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

W+glCIH^R>~(0>znbXoe3GX34a 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%0DEzF2C=_ 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%0Dn`dQ3;r7DIK$DgGJ-#$x#>dS9Kq+Ne&$K$k|kp{ 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