From 2dcd4c24aaf4c9bd5691140397ee32ef31911bbd Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Sun, 3 Apr 2022 10:07:39 -0700 Subject: [PATCH 01/11] VAULT-5422: Add rate limit for TOTP passcode attempts --- command/server/config.go | 16 +++++ command/server/config_test_helpers.go | 6 +- command/server/test-fixtures/config.hcl | 1 + vault/core.go | 10 ++- .../identity/login_mfa_totp_test.go | 24 +++++++ vault/login_mfa.go | 63 +++++++++++++------ 6 files changed, 97 insertions(+), 23 deletions(-) diff --git a/command/server/config.go b/command/server/config.go index 92a911185231..2264720fe6a0 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -80,6 +80,9 @@ type Config struct { LogRequestsLevel string `hcl:"-"` LogRequestsLevelRaw interface{} `hcl:"log_requests_level"` + MaximumTOTPValidationAttempts int64 `hcl:"-"` + MaximumTOTPValidationAttemptsRaw interface{} `hcl:"max_totp_validation_attempts"` + EnableResponseHeaderRaftNodeID bool `hcl:"-"` EnableResponseHeaderRaftNodeIDRaw interface{} `hcl:"enable_response_header_raft_node_id"` @@ -301,6 +304,11 @@ func (c *Config) Merge(c2 *Config) *Config { result.LogRequestsLevel = c2.LogRequestsLevel } + result.MaximumTOTPValidationAttempts = c.MaximumTOTPValidationAttempts + if c2.MaximumTOTPValidationAttempts != 0 { + result.MaximumTOTPValidationAttempts = c2.MaximumTOTPValidationAttempts + } + result.EnableResponseHeaderRaftNodeID = c.EnableResponseHeaderRaftNodeID if c2.EnableResponseHeaderRaftNodeID { result.EnableResponseHeaderRaftNodeID = c2.EnableResponseHeaderRaftNodeID @@ -494,6 +502,14 @@ func ParseConfig(d, source string) (*Config, error) { result.LogRequestsLevelRaw = "" } + if result.MaximumTOTPValidationAttemptsRaw != nil { + if result.MaximumTOTPValidationAttempts, err = parseutil.ParseInt(result.MaximumTOTPValidationAttemptsRaw); err != nil { + return nil, err + } + + result.MaximumTOTPValidationAttemptsRaw = nil + } + if result.EnableResponseHeaderRaftNodeIDRaw != nil { if result.EnableResponseHeaderRaftNodeID, err = parseutil.ParseBool(result.EnableResponseHeaderRaftNodeIDRaw); err != nil { return nil, err diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index 30260466feb5..49268e3cca78 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -449,6 +449,8 @@ func testLoadConfigFile(t *testing.T) { EnableUI: true, EnableUIRaw: true, + MaximumTOTPValidationAttempts: 10, + EnableRawEndpoint: true, EnableRawEndpointRaw: true, @@ -487,8 +489,8 @@ func testUnknownFieldValidation(t *testing.T) { Problem: "unknown or unsupported field bad_value found in configuration", Position: token.Pos{ Filename: "./test-fixtures/config.hcl", - Offset: 583, - Line: 34, + Offset: 617, + Line: 35, Column: 5, }, }, diff --git a/command/server/test-fixtures/config.hcl b/command/server/test-fixtures/config.hcl index 3b4123faeacc..dceeaa43065f 100644 --- a/command/server/test-fixtures/config.hcl +++ b/command/server/test-fixtures/config.hcl @@ -2,6 +2,7 @@ disable_cache = true disable_mlock = true ui = true +max_totp_validation_attempts = 10 listener "tcp" { address = "127.0.0.1:443" diff --git a/vault/core.go b/vault/core.go index bf2615770842..77fc66b7b4df 100644 --- a/vault/core.go +++ b/vault/core.go @@ -80,6 +80,10 @@ const ( // MfaAuthResponse when the value is not specified in the server config defaultMFAAuthResponseTTL = 300 * time.Second + // defaultMaxTOTPValidateAttempts is the default value for the number + // of failed attempts to validate a request subject to TOTP MFA + defaultMaxTOTPValidateAttempts = 5 + // ForwardSSCTokenToActive is the value that must be set in the // forwardToActive to trigger forwarding if a perf standby encounters // an SSC Token that it does not have the WAL state for. @@ -1004,7 +1008,11 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.ha = conf.HAPhysical } - c.loginMFABackend = NewLoginMFABackend(c, conf.Logger) + maxTOTPValidationAttempts := conf.RawConfig.MaximumTOTPValidationAttempts + if maxTOTPValidationAttempts == 0 { + maxTOTPValidationAttempts = defaultMaxTOTPValidateAttempts + } + c.loginMFABackend = NewLoginMFABackend(c, conf.Logger, maxTOTPValidationAttempts) logicalBackends := make(map[string]logical.Factory) for k, f := range conf.LogicalBackends { diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index ace0633f1fe4..aa254906a6be 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -315,6 +315,30 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { t.Fatalf("expected error message to mention code already used") } + // check for reaching max failed validation requests + secret, err = user2Client.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } + + var maxErr error + for i := 0; i < 6; i++ { + _, maxErr = user2Client.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {fmt.Sprintf("%d", i)}, + }, + }) + if maxErr == nil { + t.Fatalf("MFA succeeded with an invalid passcode") + } + } + if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 6 exceeded 5") { + t.Fatalf("unexpected error message when exceeding max failed validation attempts") + } + // Destroy the secret so that the token can self generate _, err = userClient.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ "entity_id": entityID, diff --git a/vault/login_mfa.go b/vault/login_mfa.go index 45092ef2ad64..8be87e68dcc7 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -96,13 +96,14 @@ func genericOptionalUUIDRegex(name string) string { } type MFABackend struct { - Core *Core - mfaLock *sync.RWMutex - db *memdb.MemDB - mfaLogger hclog.Logger - namespacer Namespacer - methodTable string - usedCodes *cache.Cache + Core *Core + mfaLock *sync.RWMutex + db *memdb.MemDB + mfaLogger hclog.Logger + namespacer Namespacer + methodTable string + usedCodes *cache.Cache + maximumTOTPValidationAttempts int64 } type LoginMFABackend struct { @@ -116,12 +117,12 @@ func loginMFASchemaFuncs() []func() *memdb.TableSchema { } } -func NewLoginMFABackend(core *Core, logger hclog.Logger) *LoginMFABackend { - b := NewMFABackend(core, logger, memDBLoginMFAConfigsTable, loginMFASchemaFuncs()) +func NewLoginMFABackend(core *Core, logger hclog.Logger, maxTOTPValidationAttempts int64) *LoginMFABackend { + b := NewMFABackend(core, logger, memDBLoginMFAConfigsTable, loginMFASchemaFuncs(), maxTOTPValidationAttempts) return &LoginMFABackend{b} } -func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs []func() *memdb.TableSchema) *MFABackend { +func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs []func() *memdb.TableSchema, maxTOTPValidationAttempts int64) *MFABackend { mfaSchemas := &memdb.DBSchema{ Tables: make(map[string]*memdb.TableSchema), } @@ -136,12 +137,13 @@ func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs [ db, _ := memdb.NewMemDB(mfaSchemas) return &MFABackend{ - Core: core, - mfaLock: &sync.RWMutex{}, - db: db, - mfaLogger: logger.Named("mfa"), - namespacer: core, - methodTable: prefix, + Core: core, + mfaLock: &sync.RWMutex{}, + db: db, + mfaLogger: logger.Named("mfa"), + namespacer: core, + methodTable: prefix, + maximumTOTPValidationAttempts: maxTOTPValidationAttempts, } } @@ -1425,7 +1427,7 @@ func (c *Core) validateLoginMFAInternal(ctx context.Context, methodID string, en return fmt.Errorf("MFA credentials not supplied") } - return c.validateTOTP(ctx, mfaCreds, entityMFASecret, mConfig.ID, entity.ID) + return c.validateTOTP(ctx, mfaCreds, entityMFASecret, mConfig.ID, entity.ID, c.loginMFABackend.usedCodes, c.loginMFABackend.maximumTOTPValidationAttempts) case mfaMethodTypeOkta: return c.validateOkta(ctx, mConfig, finalUsername) @@ -1997,7 +1999,7 @@ func (c *Core) validatePingID(ctx context.Context, mConfig *mfa.Config, username return nil } -func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSecret *mfa.Secret, configID, entityID string) error { +func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSecret *mfa.Secret, configID, entityID string, usedCodes *cache.Cache, maximumValidationAttempts int64) error { if len(creds) == 0 { return fmt.Errorf("missing TOTP passcode") } @@ -2013,11 +2015,29 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec usedName := fmt.Sprintf("%s_%s", configID, creds[0]) - _, ok := c.loginMFABackend.usedCodes.Get(usedName) + _, ok := usedCodes.Get(usedName) if ok { return fmt.Errorf("code already used; new code is available in %v seconds", totpSecret.Period) } + numAttempts, _ := usedCodes.Get(configID) + if numAttempts == nil { + usedCodes.Set(configID, int64(1), defaultMFAAuthResponseTTL) + } else { + err := usedCodes.Increment(configID, 1) + if err != nil { + return fmt.Errorf("failed to increment the TOTP code counter") + } + } + numAttempts, _ = usedCodes.Get(configID) + num, ok := numAttempts.(int64) + if !ok { + return fmt.Errorf("invalid counter type returned in TOTP usedCode cache") + } + if num > maximumValidationAttempts { + return fmt.Errorf("maximum TOTP validation attempts %d exceeded %d", num, maximumValidationAttempts) + } + key, err := c.fetchTOTPKey(ctx, configID, entityID) if err != nil { return errwrap.Wrapf("error fetching TOTP key: {{err}}", err) @@ -2048,11 +2068,14 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec validityPeriod := time.Duration(int64(time.Second) * int64(totpSecret.Period) * int64(2+totpSecret.Skew)) // Adding the used code to the cache - err = c.loginMFABackend.usedCodes.Add(usedName, nil, validityPeriod) + err = usedCodes.Add(usedName, nil, validityPeriod) if err != nil { return fmt.Errorf("error adding code to used cache: %w", err) } + // resetting the number of attempts to 0 after a successful validation + usedCodes.Set(configID, int64(0), defaultMFAAuthResponseTTL) + return nil } From e17103b337c85a932b07f5946bab11246840c80f Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Sun, 3 Apr 2022 10:24:09 -0700 Subject: [PATCH 02/11] fixing the docs --- website/content/docs/auth/login-mfa/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/content/docs/auth/login-mfa/index.mdx b/website/content/docs/auth/login-mfa/index.mdx index 207ec836df1e..378b699cfcf4 100644 --- a/website/content/docs/auth/login-mfa/index.mdx +++ b/website/content/docs/auth/login-mfa/index.mdx @@ -43,7 +43,7 @@ MFA in Vault includes the following login types: TOTP passcodes by default. We recommend that per-client [rate limits](/docs/concepts/resource-quotas) are applied to the relevant login and/or mfa paths (e.g. `/sys/mfa/validate`). External MFA methods (`Duo`, `Ping` and `Okta`) may already provide configurable rate limiting. Rate limiting of -Login MFA paths will be enforced by default in a future release. +Login MFA paths will be enforced by default from 10.1.0 release. Login MFA can be configured to secure further authenticating to an auth method. To enable login MFA, an MFA method needs to be configured. Please see [Login MFA API](/api-docs/secret/identity/mfa) for details From 04120ca693b5ce38997feee613a48883be2c058d Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Sun, 3 Apr 2022 10:27:22 -0700 Subject: [PATCH 03/11] CL --- changelog/14864.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/14864.txt diff --git a/changelog/14864.txt b/changelog/14864.txt new file mode 100644 index 000000000000..5c76d3c1d39d --- /dev/null +++ b/changelog/14864.txt @@ -0,0 +1,3 @@ +```release-note:improvement +Login MFA: enforce a rate limit for TOTP passcode validation attempts +``` From 1f4dc01f494ef39396cb19fcfe333e8d484d29e7 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Tue, 5 Apr 2022 14:31:26 -0700 Subject: [PATCH 04/11] feedback --- changelog/14864.txt | 2 +- vault/core.go | 4 +++- .../identity/login_mfa_totp_test.go | 2 +- vault/login_mfa.go | 23 +++++++++++-------- website/content/docs/auth/login-mfa/index.mdx | 18 ++++++++++++++- 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/changelog/14864.txt b/changelog/14864.txt index 5c76d3c1d39d..34af10f1e92f 100644 --- a/changelog/14864.txt +++ b/changelog/14864.txt @@ -1,3 +1,3 @@ ```release-note:improvement -Login MFA: enforce a rate limit for TOTP passcode validation attempts +auth: enforce a rate limit for TOTP passcode validation attempts ``` diff --git a/vault/core.go b/vault/core.go index 77fc66b7b4df..c9e79d664d70 100644 --- a/vault/core.go +++ b/vault/core.go @@ -81,7 +81,9 @@ const ( defaultMFAAuthResponseTTL = 300 * time.Second // defaultMaxTOTPValidateAttempts is the default value for the number - // of failed attempts to validate a request subject to TOTP MFA + // of failed attempts to validate a request subject to TOTP MFA. If the + // number of failed totp passcode validations exceeds this max value, the + // user needs to wait until a fresh totp passcode is generated. defaultMaxTOTPValidateAttempts = 5 // ForwardSSCTokenToActive is the value that must be set in the diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index aa254906a6be..0edd3544acb4 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -335,7 +335,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { t.Fatalf("MFA succeeded with an invalid passcode") } } - if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 6 exceeded 5") { + if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 6 exceeded the allowed attempts 5") { t.Fatalf("unexpected error message when exceeding max failed validation attempts") } diff --git a/vault/login_mfa.go b/vault/login_mfa.go index 8be87e68dcc7..d5e7079ceba8 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -2020,23 +2020,26 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec return fmt.Errorf("code already used; new code is available in %v seconds", totpSecret.Period) } + // The duration in which a passcode is stored in cache to enforce + // rate limit on failed totp passcode validation + passcodeTTL := time.Duration(int64(time.Second) * int64(totpSecret.Period)) + numAttempts, _ := usedCodes.Get(configID) if numAttempts == nil { - usedCodes.Set(configID, int64(1), defaultMFAAuthResponseTTL) + usedCodes.Set(configID, int64(1), passcodeTTL) } else { + num, ok := numAttempts.(int64) + if !ok { + return fmt.Errorf("invalid counter type returned in TOTP usedCode cache") + } + if num == maximumValidationAttempts { + return fmt.Errorf("maximum TOTP validation attempts %d exceeded the allowed attempts %d", num+1, maximumValidationAttempts) + } err := usedCodes.Increment(configID, 1) if err != nil { return fmt.Errorf("failed to increment the TOTP code counter") } } - numAttempts, _ = usedCodes.Get(configID) - num, ok := numAttempts.(int64) - if !ok { - return fmt.Errorf("invalid counter type returned in TOTP usedCode cache") - } - if num > maximumValidationAttempts { - return fmt.Errorf("maximum TOTP validation attempts %d exceeded %d", num, maximumValidationAttempts) - } key, err := c.fetchTOTPKey(ctx, configID, entityID) if err != nil { @@ -2074,7 +2077,7 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec } // resetting the number of attempts to 0 after a successful validation - usedCodes.Set(configID, int64(0), defaultMFAAuthResponseTTL) + usedCodes.Set(configID, int64(0), passcodeTTL) return nil } diff --git a/website/content/docs/auth/login-mfa/index.mdx b/website/content/docs/auth/login-mfa/index.mdx index 378b699cfcf4..d51b03187499 100644 --- a/website/content/docs/auth/login-mfa/index.mdx +++ b/website/content/docs/auth/login-mfa/index.mdx @@ -43,7 +43,7 @@ MFA in Vault includes the following login types: TOTP passcodes by default. We recommend that per-client [rate limits](/docs/concepts/resource-quotas) are applied to the relevant login and/or mfa paths (e.g. `/sys/mfa/validate`). External MFA methods (`Duo`, `Ping` and `Okta`) may already provide configurable rate limiting. Rate limiting of -Login MFA paths will be enforced by default from 10.1.0 release. +Login MFA paths are enforced by default in Vault 1.10.1 and above. Login MFA can be configured to secure further authenticating to an auth method. To enable login MFA, an MFA method needs to be configured. Please see [Login MFA API](/api-docs/secret/identity/mfa) for details @@ -190,3 +190,19 @@ $ vault write -non-interactive sys/mfa/validate -format=json @payload.json ``` To get started with Login MFA, refer to the [Login MFA](https://learn.hashicorp.com/tutorials/vault/multi-factor-authentication) tutorial. + + +### TOTP Passcode Validation Rate Limit + +By default, Vault allows for 5 consecutive failed TOTP passcode validation. +This value can also be configured by adding `max_totp_validation_attempts` to the server configuration file as shown below. +If the number of consecutive failed TOTP passcode validation exceeds the configured value, the user +needs to wait until a fresh TOTP passcode is available. + +```hcl +max_totp_validation_attempts = 10 + +listener "tcp" { + # ... +} +``` From 01d8fd32f581336d5973354d5a80cfe32f1c2cef Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Tue, 5 Apr 2022 14:36:32 -0700 Subject: [PATCH 05/11] Additional info in doc --- website/content/docs/auth/login-mfa/index.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/website/content/docs/auth/login-mfa/index.mdx b/website/content/docs/auth/login-mfa/index.mdx index d51b03187499..2ff12975071a 100644 --- a/website/content/docs/auth/login-mfa/index.mdx +++ b/website/content/docs/auth/login-mfa/index.mdx @@ -194,6 +194,7 @@ To get started with Login MFA, refer to the [Login MFA](https://learn.hashicorp. ### TOTP Passcode Validation Rate Limit +Rate limiting of Login MFA paths are enforced by default in Vault 1.10.1 and above. By default, Vault allows for 5 consecutive failed TOTP passcode validation. This value can also be configured by adding `max_totp_validation_attempts` to the server configuration file as shown below. If the number of consecutive failed TOTP passcode validation exceeds the configured value, the user From 6d3142b1c9d228fae6b6348d12d07b2df71bf764 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Wed, 6 Apr 2022 13:19:59 -0700 Subject: [PATCH 06/11] rate limit is done per entity per methodID --- .../identity/login_mfa_totp_test.go | 444 +++++++++--------- vault/login_mfa.go | 13 +- 2 files changed, 235 insertions(+), 222 deletions(-) diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index 0edd3544acb4..fdb91fc24bed 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -16,6 +16,59 @@ import ( "github.com/hashicorp/vault/vault" ) +func createEntityAndAlias(client *api.Client, mountAccessor, entityName, aliasName string, t *testing.T) (*api.Client, string) { + _, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("auth/userpass/users/%s", aliasName), map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("failed to configure userpass backend: %v", err) + } + + userClient, err := client.Clone() + if err != nil { + t.Fatalf("failed to clone the client") + } + userClient.SetToken(client.Token()) + + resp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity", map[string]interface{}{ + "name": entityName, + }) + if err != nil { + t.Fatalf("failed to create an entity") + } + entityID := resp.Data["id"].(string) + + _, err = client.Logical().WriteWithContext(context.Background(), "identity/entity-alias", map[string]interface{}{ + "name": aliasName, + "canonical_id": entityID, + "mount_accessor": mountAccessor, + }) + if err != nil { + t.Fatalf("failed to create an entity alias") + } + return userClient, entityID +} + +func registerEntityInTOTPEngine(client *api.Client, entityID, methodID string, t *testing.T) string { + totpGenName := fmt.Sprintf("%s-%s", entityID, methodID) + secret, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-generate"), map[string]interface{}{ + "entity_id": entityID, + "method_id": methodID, + }) + if err != nil { + t.Fatalf("failed to generate a TOTP secret on an entity: %v", err) + } + totpURL := secret.Data["url"].(string) + + _, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("totp/keys/%s", totpGenName), map[string]interface{}{ + "url": totpURL, + }) + if err != nil { + t.Fatalf("failed to register a TOTP URL: %v", err) + } + return totpGenName +} + func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { var noop *vault.NoopAudit @@ -67,14 +120,6 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { t.Fatalf("failed to enable userpass auth: %v", err) } - // Creating a user in the userpass auth mount - _, err = client.Logical().WriteWithContext(context.Background(), "auth/userpass/users/testuser", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("failed to configure userpass backend: %v", err) - } - auths, err := client.Sys().ListAuthWithContext(context.Background()) if err != nil { t.Fatalf("bb") @@ -84,52 +129,12 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { mountAccessor = auths["userpass/"].Accessor } - userClient, err := client.Clone() - if err != nil { - t.Fatalf("failed to clone the client") - } - userClient.SetToken(client.Token()) - - var entityID string - var groupID string - { - resp, err := userClient.Logical().WriteWithContext(context.Background(), "identity/entity", map[string]interface{}{ - "name": "test-entity", - "metadata": map[string]string{ - "email": "test@hashicorp.com", - "phone_number": "123-456-7890", - }, - }) - if err != nil { - t.Fatalf("failed to create an entity") - } - entityID = resp.Data["id"].(string) - - // Create a group - resp, err = client.Logical().WriteWithContext(context.Background(), "identity/group", map[string]interface{}{ - "name": "engineering", - "member_entity_ids": []string{entityID}, - }) - if err != nil { - t.Fatalf("failed to create an identity group") - } - groupID = resp.Data["id"].(string) - - _, err = client.Logical().WriteWithContext(context.Background(), "identity/entity-alias", map[string]interface{}{ - "name": "testuser", - "canonical_id": entityID, - "mount_accessor": mountAccessor, - }) - if err != nil { - t.Fatalf("failed to create an entity alias") - } - - } + // Creating two users in the userpass auth mount + userClient1, entityID1 := createEntityAndAlias(client, mountAccessor, "entity1", "testuser1", t) + userClient2, entityID2 := createEntityAndAlias(client, mountAccessor, "entity2", "testuser2", t) // configure TOTP secret engine - var totpPasscode string var methodID string - var userpassToken string // login MFA { // create a config @@ -152,200 +157,205 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { t.Fatalf("method ID is empty") } - secret, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-generate"), map[string]interface{}{ - "entity_id": entityID, - "method_id": methodID, - }) - if err != nil { - t.Fatalf("failed to generate a TOTP secret on an entity: %v", err) - } - totpURL := secret.Data["url"].(string) - - _, err = client.Logical().WriteWithContext(context.Background(), "totp/keys/loginMFA", map[string]interface{}{ - "url": totpURL, - }) - if err != nil { - t.Fatalf("failed to register a TOTP URL: %v", err) - } - - secret, err = client.Logical().ReadWithContext(context.Background(), "totp/code/loginMFA") - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - totpPasscode = secret.Data["code"].(string) - // creating MFAEnforcementConfig _, err = client.Logical().WriteWithContext(context.Background(), "identity/mfa/login-enforcement/randomName", map[string]interface{}{ - "auth_method_accessors": []string{mountAccessor}, - "auth_method_types": []string{"userpass"}, - "identity_group_ids": []string{groupID}, - "identity_entity_ids": []string{entityID}, - "name": "randomName", - "mfa_method_ids": []string{methodID}, + "auth_method_types": []string{"userpass"}, + "name": "randomName", + "mfa_method_ids": []string{methodID}, }) if err != nil { t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) } + } - // MFA single-phase login - userClient.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", methodID, totpPasscode)) - secret, err = userClient.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) - } + // registering EntityIDs in the TOTP secret Engine for MethodID + totpEngineConfigName1 := registerEntityInTOTPEngine(client, entityID1, methodID, t) + totpEngineConfigName2 := registerEntityInTOTPEngine(client, entityID2, methodID, t) - userpassToken = secret.Auth.ClientToken + // MFA single-phase login + totpCodePath1 := fmt.Sprintf("totp/code/%s", totpEngineConfigName1) + secret, err := client.Logical().ReadWithContext(context.Background(), totpCodePath1) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + totpPasscode1 := secret.Data["code"].(string) - userClient.SetToken(client.Token()) - secret, err = userClient.Logical().WriteWithContext(context.Background(), "auth/token/lookup", map[string]interface{}{ - "token": userpassToken, - }) - if err != nil { - t.Fatalf("failed to lookup userpass authenticated token: %v", err) - } + userClient1.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", methodID, totpPasscode1)) + secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } - entityIDCheck := secret.Data["entity_id"].(string) - if entityIDCheck != entityID { - t.Fatalf("different entityID assigned") - } + userpassToken := secret.Auth.ClientToken - // Two-phase login - user2Client, err := client.Clone() - if err != nil { - t.Fatalf("failed to clone the client") - } - headers := user2Client.Headers() - headers.Del("X-Vault-MFA") - user2Client.SetHeaders(headers) - secret, err = user2Client.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) - } + userClient1.SetToken(client.Token()) + secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/token/lookup", map[string]interface{}{ + "token": userpassToken, + }) + if err != nil { + t.Fatalf("failed to lookup userpass authenticated token: %v", err) + } - if len(secret.Warnings) == 0 || !strings.Contains(strings.Join(secret.Warnings, ""), "A login request was issued that is subject to MFA validation") { - t.Fatalf("first phase of login did not have a warning") - } + entityIDCheck := secret.Data["entity_id"].(string) + if entityIDCheck != entityID1 { + t.Fatalf("different entityID assigned") + } - if secret.Auth == nil || secret.Auth.MFARequirement == nil { - t.Fatalf("two phase login returned nil MFARequirement") - } - if secret.Auth.MFARequirement.MFARequestID == "" { - t.Fatalf("MFARequirement contains empty MFARequestID") - } - if secret.Auth.MFARequirement.MFAConstraints == nil || len(secret.Auth.MFARequirement.MFAConstraints) == 0 { - t.Fatalf("MFAConstraints is nil or empty") - } - mfaConstraints, ok := secret.Auth.MFARequirement.MFAConstraints["randomName"] - if !ok { - t.Fatalf("failed to find the mfaConstrains") - } - if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 { - t.Fatalf("") - } - for _, mfaAny := range mfaConstraints.Any { - if mfaAny.ID != methodID || mfaAny.Type != "totp" || !mfaAny.UsesPasscode { - t.Fatalf("Invalid mfa constraints") - } - } + // Two-phase login + headers := userClient1.Headers() + headers.Del("X-Vault-MFA") + userClient1.SetHeaders(headers) + secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } - // validation - // waiting for 5 seconds so that a fresh code could be generated - time.Sleep(5 * time.Second) - // getting a fresh totp passcode for the validation step - totpResp, err := client.Logical().ReadWithContext(context.Background(), "totp/code/loginMFA") - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - totpPasscode = totpResp.Data["code"].(string) + if len(secret.Warnings) == 0 || !strings.Contains(strings.Join(secret.Warnings, ""), "A login request was issued that is subject to MFA validation") { + t.Fatalf("first phase of login did not have a warning") + } - secret, err = user2Client.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": map[string][]string{ - methodID: {totpPasscode}, - }, - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) + if secret.Auth == nil || secret.Auth.MFARequirement == nil { + t.Fatalf("two phase login returned nil MFARequirement") + } + if secret.Auth.MFARequirement.MFARequestID == "" { + t.Fatalf("MFARequirement contains empty MFARequestID") + } + if secret.Auth.MFARequirement.MFAConstraints == nil || len(secret.Auth.MFARequirement.MFAConstraints) == 0 { + t.Fatalf("MFAConstraints is nil or empty") + } + mfaConstraints, ok := secret.Auth.MFARequirement.MFAConstraints["randomName"] + if !ok { + t.Fatalf("failed to find the mfaConstrains") + } + if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 { + t.Fatalf("") + } + for _, mfaAny := range mfaConstraints.Any { + if mfaAny.ID != methodID || mfaAny.Type != "totp" || !mfaAny.UsesPasscode { + t.Fatalf("Invalid mfa constraints") } + } - if secret.Auth == nil || secret.Auth.ClientToken == "" { - t.Fatalf("successful mfa validation did not return a client token") - } + // validation + // waiting for 5 seconds so that a fresh code could be generated + time.Sleep(5 * time.Second) + // getting a fresh totp passcode for the validation step + totpResp, err := client.Logical().ReadWithContext(context.Background(), totpCodePath1) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + totpPasscode1 = totpResp.Data["code"].(string) - if noop.Req == nil { - t.Fatalf("no request was logged in audit log") - } - var found bool - for _, req := range noop.Req { - if req.Path == "sys/mfa/validate" { - found = true - break - } - } - if !found { - t.Fatalf("mfa/validate was not logged in audit log") - } + secret, err = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {totpPasscode1}, + }, + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } - // check for login request expiration - secret, err = user2Client.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) - } + if secret.Auth == nil || secret.Auth.ClientToken == "" { + t.Fatalf("successful mfa validation did not return a client token") + } - if secret.Auth == nil || secret.Auth.MFARequirement == nil { - t.Fatalf("two phase login returned nil MFARequirement") + if noop.Req == nil { + t.Fatalf("no request was logged in audit log") + } + var found bool + for _, req := range noop.Req { + if req.Path == "sys/mfa/validate" { + found = true + break } + } + if !found { + t.Fatalf("mfa/validate was not logged in audit log") + } + + // check for login request expiration + secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } + + if secret.Auth == nil || secret.Auth.MFARequirement == nil { + t.Fatalf("two phase login returned nil MFARequirement") + } + + _, err = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {totpPasscode1}, + }, + }) + if err == nil { + t.Fatalf("MFA succeeded with an already used passcode") + } + if !strings.Contains(err.Error(), "code already used") { + t.Fatalf("expected error message to mention code already used") + } - _, err = user2Client.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ + // check for reaching max failed validation requests + secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } + + var maxErr error + for i := 0; i < 6; i++ { + _, maxErr = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, "mfa_payload": map[string][]string{ - methodID: {totpPasscode}, + methodID: {fmt.Sprintf("%d", i)}, }, }) - if err == nil { - t.Fatalf("MFA succeeded with an already used passcode") - } - if !strings.Contains(err.Error(), "code already used") { - t.Fatalf("expected error message to mention code already used") - } - - // check for reaching max failed validation requests - secret, err = user2Client.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) + if maxErr == nil { + t.Fatalf("MFA succeeded with an invalid passcode") } + } + if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 6 exceeded the allowed attempts 5") { + t.Fatalf("unexpected error message when exceeding max failed validation attempts") + } - var maxErr error - for i := 0; i < 6; i++ { - _, maxErr = user2Client.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": map[string][]string{ - methodID: {fmt.Sprintf("%d", i)}, - }, - }) - if maxErr == nil { - t.Fatalf("MFA succeeded with an invalid passcode") - } - } - if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 6 exceeded the allowed attempts 5") { - t.Fatalf("unexpected error message when exceeding max failed validation attempts") - } + // let's make sure the configID is not blocked for other users + totpCodePath2 := fmt.Sprintf("totp/code/%s", totpEngineConfigName2) + totpResp, err = userClient2.Logical().ReadWithContext(context.Background(), totpCodePath2) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + totpPasscode2 := totpResp.Data["code"].(string) + secret, err = userClient2.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser2", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } + secret, err = userClient2.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {totpPasscode2}, + }, + }) + if err != nil { + t.Fatalf("MFA failed: %v", err) + } - // Destroy the secret so that the token can self generate - _, err = userClient.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ - "entity_id": entityID, - "method_id": methodID, - }) - if err != nil { - t.Fatalf("failed to destroy the MFA secret: %s", err) - } + // Destroy the secret so that the token can self generate + _, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ + "entity_id": entityID1, + "method_id": methodID, + }) + if err != nil { + t.Fatalf("failed to destroy the MFA secret: %s", err) } } diff --git a/vault/login_mfa.go b/vault/login_mfa.go index d5e7079ceba8..146ca484dc9c 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -2024,18 +2024,21 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec // rate limit on failed totp passcode validation passcodeTTL := time.Duration(int64(time.Second) * int64(totpSecret.Period)) - numAttempts, _ := usedCodes.Get(configID) + // Enforcing rate limit per MethodID per EntityID + rateLimitID := fmt.Sprintf("%s_%s", configID, entityID) + + numAttempts, _ := usedCodes.Get(rateLimitID) if numAttempts == nil { - usedCodes.Set(configID, int64(1), passcodeTTL) + usedCodes.Set(rateLimitID, int64(1), passcodeTTL) } else { num, ok := numAttempts.(int64) if !ok { return fmt.Errorf("invalid counter type returned in TOTP usedCode cache") } if num == maximumValidationAttempts { - return fmt.Errorf("maximum TOTP validation attempts %d exceeded the allowed attempts %d", num+1, maximumValidationAttempts) + return fmt.Errorf("maximum TOTP validation attempts %d exceeded the allowed attempts %d. Please try again in %v seconds", num+1, maximumValidationAttempts, passcodeTTL) } - err := usedCodes.Increment(configID, 1) + err := usedCodes.Increment(rateLimitID, 1) if err != nil { return fmt.Errorf("failed to increment the TOTP code counter") } @@ -2077,7 +2080,7 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec } // resetting the number of attempts to 0 after a successful validation - usedCodes.Set(configID, int64(0), passcodeTTL) + usedCodes.Set(rateLimitID, int64(0), passcodeTTL) return nil } From 9458f2893215a66ea58464d1605aa6bb2399a7aa Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Wed, 6 Apr 2022 14:28:00 -0700 Subject: [PATCH 07/11] refactoring a test --- .../identity/login_mfa_totp_test.go | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index fdb91fc24bed..4fade9031fe6 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -69,6 +69,30 @@ func registerEntityInTOTPEngine(client *api.Client, entityID, methodID string, t return totpGenName } +func doTwoPhaseLogin(client *api.Client, totpCodePath, methodID, username string, t *testing.T) { + totpResp, err := client.Logical().ReadWithContext(context.Background(), totpCodePath) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + totpPasscode := totpResp.Data["code"].(string) + + secret, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("auth/userpass/login/%s", username), map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("first phase of login MFA failed: %v", err) + } + secret, err = client.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {totpPasscode}, + }, + }) + if err != nil { + t.Fatalf("MFA validation failed: %v", err) + } +} + func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { var noop *vault.NoopAudit @@ -329,26 +353,12 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { // let's make sure the configID is not blocked for other users totpCodePath2 := fmt.Sprintf("totp/code/%s", totpEngineConfigName2) - totpResp, err = userClient2.Logical().ReadWithContext(context.Background(), totpCodePath2) - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - totpPasscode2 := totpResp.Data["code"].(string) - secret, err = userClient2.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser2", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) - } - secret, err = userClient2.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": map[string][]string{ - methodID: {totpPasscode2}, - }, - }) - if err != nil { - t.Fatalf("MFA failed: %v", err) - } + doTwoPhaseLogin(userClient2, totpCodePath2, methodID, "testuser2", t) + + // let's see if user1 is able to login after 5 seconds + time.Sleep(5 * time.Second) + // getting a fresh totp passcode for the validation step + doTwoPhaseLogin(userClient1, totpCodePath1, methodID, "testuser1", t) // Destroy the secret so that the token can self generate _, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ From 837f8555df88ca5169085dcc75dee1c73362128e Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Wed, 6 Apr 2022 16:23:40 -0700 Subject: [PATCH 08/11] rate limit OSS work for policy MFA --- vault/core.go | 9 +++++++++ vault/login_mfa.go | 18 +++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/vault/core.go b/vault/core.go index c9e79d664d70..d6cd00b8706d 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1033,6 +1033,9 @@ func NewCore(conf *CoreConfig) (*Core, error) { if err := b.Setup(ctx, config); err != nil { return nil, err } + if b.mfaBackend != nil { + b.mfaBackend.maximumTOTPValidationAttempts = maxTOTPValidationAttempts + } return b, nil } logicalBackends["identity"] = func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) { @@ -2256,6 +2259,9 @@ func (c *Core) postUnseal(ctx context.Context, ctxCancelFunc context.CancelFunc, c.logger.Warn("disabling entities for local auth mounts through env var", "env", EnvVaultDisableLocalAuthMountEntities) } c.loginMFABackend.usedCodes = cache.New(0, 30*time.Second) + if c.systemBackend.mfaBackend != nil { + c.systemBackend.mfaBackend.usedCodes = cache.New(0, 30*time.Second) + } c.logger.Info("post-unseal setup complete") return nil } @@ -2332,6 +2338,9 @@ func (c *Core) preSeal() error { } c.loginMFABackend.usedCodes = nil + if c.systemBackend.mfaBackend != nil { + c.systemBackend.mfaBackend.usedCodes = nil + } preSealPhysical(c) c.logger.Info("pre-seal teardown complete") diff --git a/vault/login_mfa.go b/vault/login_mfa.go index 146ca484dc9c..dd0c030f3ed4 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -118,11 +118,12 @@ func loginMFASchemaFuncs() []func() *memdb.TableSchema { } func NewLoginMFABackend(core *Core, logger hclog.Logger, maxTOTPValidationAttempts int64) *LoginMFABackend { - b := NewMFABackend(core, logger, memDBLoginMFAConfigsTable, loginMFASchemaFuncs(), maxTOTPValidationAttempts) + b := NewMFABackend(core, logger, memDBLoginMFAConfigsTable, loginMFASchemaFuncs()) + b.maximumTOTPValidationAttempts = maxTOTPValidationAttempts return &LoginMFABackend{b} } -func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs []func() *memdb.TableSchema, maxTOTPValidationAttempts int64) *MFABackend { +func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs []func() *memdb.TableSchema) *MFABackend { mfaSchemas := &memdb.DBSchema{ Tables: make(map[string]*memdb.TableSchema), } @@ -137,13 +138,12 @@ func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs [ db, _ := memdb.NewMemDB(mfaSchemas) return &MFABackend{ - Core: core, - mfaLock: &sync.RWMutex{}, - db: db, - mfaLogger: logger.Named("mfa"), - namespacer: core, - methodTable: prefix, - maximumTOTPValidationAttempts: maxTOTPValidationAttempts, + Core: core, + mfaLock: &sync.RWMutex{}, + db: db, + mfaLogger: logger.Named("mfa"), + namespacer: core, + methodTable: prefix, } } From 534142f9caf27bc25773cc917eda4a5a8519e1cd Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Wed, 13 Apr 2022 07:00:04 -0700 Subject: [PATCH 09/11] adding max_validation_attempts to TOTP config --- command/server/config.go | 16 -- command/server/config_test_helpers.go | 6 +- command/server/test-fixtures/config.hcl | 1 - helper/identity/mfa/types.pb.go | 178 ++++++++++-------- helper/identity/mfa/types.proto | 2 + vault/core.go | 9 +- .../identity/login_mfa_totp_test.go | 19 +- vault/external_tests/mfa/login_mfa_test.go | 15 +- vault/identity_store.go | 4 + vault/login_mfa.go | 52 ++--- .../api-docs/secret/identity/mfa/totp.mdx | 2 + website/content/api-docs/system/mfa/totp.mdx | 3 + website/content/docs/auth/login-mfa/index.mdx | 10 +- 13 files changed, 158 insertions(+), 159 deletions(-) diff --git a/command/server/config.go b/command/server/config.go index 76036dee27b2..911dfe537ce9 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -87,9 +87,6 @@ type Config struct { LogRequestsLevel string `hcl:"-"` LogRequestsLevelRaw interface{} `hcl:"log_requests_level"` - MaximumTOTPValidationAttempts int64 `hcl:"-"` - MaximumTOTPValidationAttemptsRaw interface{} `hcl:"max_totp_validation_attempts"` - EnableResponseHeaderRaftNodeID bool `hcl:"-"` EnableResponseHeaderRaftNodeIDRaw interface{} `hcl:"enable_response_header_raft_node_id"` @@ -321,11 +318,6 @@ func (c *Config) Merge(c2 *Config) *Config { result.LogRequestsLevel = c2.LogRequestsLevel } - result.MaximumTOTPValidationAttempts = c.MaximumTOTPValidationAttempts - if c2.MaximumTOTPValidationAttempts != 0 { - result.MaximumTOTPValidationAttempts = c2.MaximumTOTPValidationAttempts - } - result.EnableResponseHeaderRaftNodeID = c.EnableResponseHeaderRaftNodeID if c2.EnableResponseHeaderRaftNodeID { result.EnableResponseHeaderRaftNodeID = c2.EnableResponseHeaderRaftNodeID @@ -553,14 +545,6 @@ func ParseConfig(d, source string) (*Config, error) { result.LogRequestsLevelRaw = "" } - if result.MaximumTOTPValidationAttemptsRaw != nil { - if result.MaximumTOTPValidationAttempts, err = parseutil.ParseInt(result.MaximumTOTPValidationAttemptsRaw); err != nil { - return nil, err - } - - result.MaximumTOTPValidationAttemptsRaw = nil - } - if result.EnableResponseHeaderRaftNodeIDRaw != nil { if result.EnableResponseHeaderRaftNodeID, err = parseutil.ParseBool(result.EnableResponseHeaderRaftNodeIDRaw); err != nil { return nil, err diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index b6dc9dace053..424d8fe816b3 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -449,8 +449,6 @@ func testLoadConfigFile(t *testing.T) { EnableUI: true, EnableUIRaw: true, - MaximumTOTPValidationAttempts: 10, - EnableRawEndpoint: true, EnableRawEndpointRaw: true, @@ -489,8 +487,8 @@ func testUnknownFieldValidation(t *testing.T) { Problem: "unknown or unsupported field bad_value found in configuration", Position: token.Pos{ Filename: "./test-fixtures/config.hcl", - Offset: 617, - Line: 35, + Offset: 583, + Line: 34, Column: 5, }, }, diff --git a/command/server/test-fixtures/config.hcl b/command/server/test-fixtures/config.hcl index dceeaa43065f..3b4123faeacc 100644 --- a/command/server/test-fixtures/config.hcl +++ b/command/server/test-fixtures/config.hcl @@ -2,7 +2,6 @@ disable_cache = true disable_mlock = true ui = true -max_totp_validation_attempts = 10 listener "tcp" { address = "127.0.0.1:443" diff --git a/helper/identity/mfa/types.pb.go b/helper/identity/mfa/types.pb.go index f82ccb46b3dd..789def20f0fe 100644 --- a/helper/identity/mfa/types.pb.go +++ b/helper/identity/mfa/types.pb.go @@ -212,6 +212,8 @@ type TOTPConfig struct { KeySize uint32 `protobuf:"varint,6,opt,name=key_size,json=keySize,proto3" json:"key_size,omitempty" sentinel:"-"` // @inject_tag: sentinel:"-" QRSize int32 `protobuf:"varint,7,opt,name=qr_size,json=qrSize,proto3" json:"qr_size,omitempty" sentinel:"-"` + // @inject_tag: sentinel:"-" + MaxValidationAttempts uint32 `protobuf:"varint,8,opt,name=max_validation_attempts,json=maxValidationAttempts,proto3" json:"max_validation_attempts,omitempty" sentinel:"-"` } func (x *TOTPConfig) Reset() { @@ -295,6 +297,13 @@ func (x *TOTPConfig) GetQRSize() int32 { return 0 } +func (x *TOTPConfig) GetMaxValidationAttempts() uint32 { + if x != nil { + return x.MaxValidationAttempts + } + return 0 +} + // DuoConfig represents the configuration information required to perform // Duo authentication. type DuoConfig struct { @@ -898,7 +907,7 @@ var file_helper_identity_mfa_types_proto_rawDesc = []byte{ 0x70, 0x69, 0x6e, 0x67, 0x69, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x42, - 0x08, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xba, 0x01, 0x0a, 0x0a, 0x54, 0x4f, + 0x08, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xf2, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54, 0x50, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, @@ -910,88 +919,91 @@ var file_helper_identity_mfa_types_proto_rawDesc = []byte{ 0x65, 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x71, 0x72, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, - 0x71, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xb6, 0x01, 0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, - 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, - 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, - 0x61, 0x70, 0x69, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x73, 0x68, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, - 0x75, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x50, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, - 0xa4, 0x01, 0x0a, 0x0a, 0x4f, 0x6b, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, - 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, - 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, - 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x64, - 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, - 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x65, 0x6d, 0x61, - 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, - 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xef, 0x01, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x49, - 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, 0x5f, 0x62, - 0x61, 0x73, 0x65, 0x36, 0x34, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0c, 0x75, 0x73, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, - 0x0d, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, - 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x70, 0x5f, - 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x64, 0x70, 0x55, 0x72, - 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x72, 0x67, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1b, - 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x61, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x6c, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x72, 0x6c, 0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x54, - 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, - 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, - 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, - 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16, 0x0a, - 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, - 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, - 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, - 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d, 0x46, - 0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, - 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, - 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73, 0x12, - 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, - 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, - 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, - 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, - 0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, - 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e, 0x0a, - 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e, 0x0a, - 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x42, 0x30, 0x5a, - 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, - 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x71, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, + 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x22, 0xb6, + 0x01, 0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, + 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, + 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x5f, 0x68, 0x6f, 0x73, 0x74, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x48, + 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x73, 0x68, 0x5f, + 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x73, 0x68, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, + 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x50, + 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x0a, 0x4f, 0x6b, 0x74, 0x61, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, + 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, + 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, + 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xef, + 0x01, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x49, 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x42, 0x61, 0x73, 0x65, + 0x36, 0x34, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x69, 0x67, + 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, + 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x70, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x69, 0x64, 0x70, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67, + 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x72, + 0x67, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, + 0x55, 0x72, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, + 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x72, 0x6c, + 0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74, + 0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, + 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54, + 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, + 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a, + 0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65, + 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d, 0x46, 0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, + 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, + 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61, + 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, + 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/helper/identity/mfa/types.proto b/helper/identity/mfa/types.proto index c20386cb9011..decade25b9af 100644 --- a/helper/identity/mfa/types.proto +++ b/helper/identity/mfa/types.proto @@ -50,6 +50,8 @@ message TOTPConfig { uint32 key_size = 6; // @inject_tag: sentinel:"-" int32 qr_size = 7; + // @inject_tag: sentinel:"-" + uint32 max_validation_attempts = 8; } // DuoConfig represents the configuration information required to perform diff --git a/vault/core.go b/vault/core.go index a561fa003799..737358d2c808 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1028,11 +1028,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.ha = conf.HAPhysical } - maxTOTPValidationAttempts := conf.RawConfig.MaximumTOTPValidationAttempts - if maxTOTPValidationAttempts == 0 { - maxTOTPValidationAttempts = defaultMaxTOTPValidateAttempts - } - c.loginMFABackend = NewLoginMFABackend(c, conf.Logger, maxTOTPValidationAttempts) + c.loginMFABackend = NewLoginMFABackend(c, conf.Logger) logicalBackends := make(map[string]logical.Factory) for k, f := range conf.LogicalBackends { @@ -1051,9 +1047,6 @@ func NewCore(conf *CoreConfig) (*Core, error) { if err := b.Setup(ctx, config); err != nil { return nil, err } - if b.mfaBackend != nil { - b.mfaBackend.maximumTOTPValidationAttempts = maxTOTPValidationAttempts - } return b, nil } logicalBackends["identity"] = func(ctx context.Context, config *logical.BackendConfig) (logical.Backend, error) { diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index cf2f4d72c364..76f9b3c7e771 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -163,13 +163,14 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { { // create a config resp1, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "yCorp", - "period": 5, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": 10, - "qr_size": 100, + "issuer": "yCorp", + "period": 5, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": 10, + "qr_size": 100, + "max_validation_attempts": 3, }) if err != nil || (resp1 == nil) { @@ -336,7 +337,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { } var maxErr error - for i := 0; i < 6; i++ { + for i := 0; i < 4; i++ { _, maxErr = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{ "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, "mfa_payload": map[string][]string{ @@ -347,7 +348,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { t.Fatalf("MFA succeeded with an invalid passcode") } } - if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 6 exceeded the allowed attempts 5") { + if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 4 exceeded the allowed attempts 3") { t.Fatalf("unexpected error message when exceeding max failed validation attempts") } diff --git a/vault/external_tests/mfa/login_mfa_test.go b/vault/external_tests/mfa/login_mfa_test.go index cd8dfd6849ed..8a971ea63eaf 100644 --- a/vault/external_tests/mfa/login_mfa_test.go +++ b/vault/external_tests/mfa/login_mfa_test.go @@ -54,13 +54,14 @@ func TestLoginMFA_Method_CRUD(t *testing.T) { { "totp", map[string]interface{}{ - "issuer": "yCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, + "issuer": "yCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + "max_validation_attempts": 1, }, "issuer", "zCorp", diff --git a/vault/identity_store.go b/vault/identity_store.go index bd0d11116e3f..c434bf9dc10c 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -148,6 +148,10 @@ func mfaPaths(i *IdentityStore) []*framework.Path { Type: framework.TypeString, Description: `The unique identifier for this MFA method.`, }, + "max_validation_attempts": { + Type: framework.TypeInt, + Description: `Max number of allowed validation attempts.`, + }, "issuer": { Type: framework.TypeString, Description: `The name of the key's issuing organization.`, diff --git a/vault/login_mfa.go b/vault/login_mfa.go index dd0c030f3ed4..599ff8af412d 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -96,14 +96,13 @@ func genericOptionalUUIDRegex(name string) string { } type MFABackend struct { - Core *Core - mfaLock *sync.RWMutex - db *memdb.MemDB - mfaLogger hclog.Logger - namespacer Namespacer - methodTable string - usedCodes *cache.Cache - maximumTOTPValidationAttempts int64 + Core *Core + mfaLock *sync.RWMutex + db *memdb.MemDB + mfaLogger hclog.Logger + namespacer Namespacer + methodTable string + usedCodes *cache.Cache } type LoginMFABackend struct { @@ -117,9 +116,8 @@ func loginMFASchemaFuncs() []func() *memdb.TableSchema { } } -func NewLoginMFABackend(core *Core, logger hclog.Logger, maxTOTPValidationAttempts int64) *LoginMFABackend { +func NewLoginMFABackend(core *Core, logger hclog.Logger) *LoginMFABackend { b := NewMFABackend(core, logger, memDBLoginMFAConfigsTable, loginMFASchemaFuncs()) - b.maximumTOTPValidationAttempts = maxTOTPValidationAttempts return &LoginMFABackend{b} } @@ -1186,6 +1184,7 @@ func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config) (map[string]interface{} respData["key_size"] = totpConfig.KeySize respData["qr_size"] = totpConfig.QRSize respData["algorithm"] = otplib.Algorithm(totpConfig.Algorithm).String() + respData["max_validation_attempts"] = totpConfig.MaxValidationAttempts case *mfa.Config_OktaConfig: oktaConfig := mConfig.GetOktaConfig() respData["org_name"] = oktaConfig.OrgName @@ -1278,14 +1277,23 @@ func parseTOTPConfig(mConfig *mfa.Config, d *framework.FieldData) error { return fmt.Errorf("issuer must be set") } + maxValidationAttempt := d.Get("max_validation_attempts").(int) + if maxValidationAttempt < 0 { + return fmt.Errorf("max_validation_attempts must be greater than zero") + } + if maxValidationAttempt == 0 { + maxValidationAttempt = defaultMaxTOTPValidateAttempts + } + config := &mfa.TOTPConfig{ - Issuer: issuer, - Period: uint32(period), - Algorithm: int32(keyAlgorithm), - Digits: int32(keyDigits), - Skew: uint32(skew), - KeySize: uint32(keySize), - QRSize: int32(d.Get("qr_size").(int)), + Issuer: issuer, + Period: uint32(period), + Algorithm: int32(keyAlgorithm), + Digits: int32(keyDigits), + Skew: uint32(skew), + KeySize: uint32(keySize), + QRSize: int32(d.Get("qr_size").(int)), + MaxValidationAttempts: uint32(maxValidationAttempt), } mConfig.Config = &mfa.Config_TOTPConfig{ TOTPConfig: config, @@ -1427,7 +1435,7 @@ func (c *Core) validateLoginMFAInternal(ctx context.Context, methodID string, en return fmt.Errorf("MFA credentials not supplied") } - return c.validateTOTP(ctx, mfaCreds, entityMFASecret, mConfig.ID, entity.ID, c.loginMFABackend.usedCodes, c.loginMFABackend.maximumTOTPValidationAttempts) + return c.validateTOTP(ctx, mfaCreds, entityMFASecret, mConfig.ID, entity.ID, c.loginMFABackend.usedCodes, mConfig.GetTOTPConfig().MaxValidationAttempts) case mfaMethodTypeOkta: return c.validateOkta(ctx, mConfig, finalUsername) @@ -1999,7 +2007,7 @@ func (c *Core) validatePingID(ctx context.Context, mConfig *mfa.Config, username return nil } -func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSecret *mfa.Secret, configID, entityID string, usedCodes *cache.Cache, maximumValidationAttempts int64) error { +func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSecret *mfa.Secret, configID, entityID string, usedCodes *cache.Cache, maximumValidationAttempts uint32) error { if len(creds) == 0 { return fmt.Errorf("missing TOTP passcode") } @@ -2029,9 +2037,9 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec numAttempts, _ := usedCodes.Get(rateLimitID) if numAttempts == nil { - usedCodes.Set(rateLimitID, int64(1), passcodeTTL) + usedCodes.Set(rateLimitID, uint32(1), passcodeTTL) } else { - num, ok := numAttempts.(int64) + num, ok := numAttempts.(uint32) if !ok { return fmt.Errorf("invalid counter type returned in TOTP usedCode cache") } @@ -2080,7 +2088,7 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec } // resetting the number of attempts to 0 after a successful validation - usedCodes.Set(rateLimitID, int64(0), passcodeTTL) + usedCodes.Set(rateLimitID, uint32(0), passcodeTTL) return nil } diff --git a/website/content/api-docs/secret/identity/mfa/totp.mdx b/website/content/api-docs/secret/identity/mfa/totp.mdx index bfd255bf223a..e170fa474ce3 100644 --- a/website/content/api-docs/secret/identity/mfa/totp.mdx +++ b/website/content/api-docs/secret/identity/mfa/totp.mdx @@ -31,6 +31,8 @@ This endpoint defines an MFA method of type TOTP. - `skew` `(int: 1)` - The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1. +- `max_validation_attempts` `(int: 5)` - The maximum number of consecutive failed validation attempts. + ### Sample Payload ```json diff --git a/website/content/api-docs/system/mfa/totp.mdx b/website/content/api-docs/system/mfa/totp.mdx index ff43832b2d3f..092f27cea601 100644 --- a/website/content/api-docs/system/mfa/totp.mdx +++ b/website/content/api-docs/system/mfa/totp.mdx @@ -32,6 +32,8 @@ This endpoint defines a MFA method of type TOTP. - `skew` `(int: 1)` - The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1. +- `max_validation_attempts` `(int: 5)` - The maximum number of consecutive TOTP code failed validation. + ### Sample Payload ```json @@ -88,6 +90,7 @@ $ curl \ "qr_size": 200, "skew": 1, "type": "totp" + "max_validation_attempts": 5 } } ``` diff --git a/website/content/docs/auth/login-mfa/index.mdx b/website/content/docs/auth/login-mfa/index.mdx index 2ff12975071a..d475cedc0130 100644 --- a/website/content/docs/auth/login-mfa/index.mdx +++ b/website/content/docs/auth/login-mfa/index.mdx @@ -196,14 +196,6 @@ To get started with Login MFA, refer to the [Login MFA](https://learn.hashicorp. Rate limiting of Login MFA paths are enforced by default in Vault 1.10.1 and above. By default, Vault allows for 5 consecutive failed TOTP passcode validation. -This value can also be configured by adding `max_totp_validation_attempts` to the server configuration file as shown below. +This value can also be configured by adding `max_validation_attempts` to the TOTP configuration. If the number of consecutive failed TOTP passcode validation exceeds the configured value, the user needs to wait until a fresh TOTP passcode is available. - -```hcl -max_totp_validation_attempts = 10 - -listener "tcp" { - # ... -} -``` From fdf26e3ab3dcc7e76675d3370099045acac65b26 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Wed, 13 Apr 2022 13:01:20 -0700 Subject: [PATCH 10/11] feedback --- vault/external_tests/identity/login_mfa_totp_test.go | 8 ++++---- vault/login_mfa.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index 76f9b3c7e771..9103bc845747 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -26,7 +26,7 @@ func createEntityAndAlias(client *api.Client, mountAccessor, entityName, aliasNa userClient, err := client.Clone() if err != nil { - t.Fatalf("failed to clone the client") + t.Fatalf("failed to clone the client:%v", err) } userClient.SetToken(client.Token()) @@ -34,7 +34,7 @@ func createEntityAndAlias(client *api.Client, mountAccessor, entityName, aliasNa "name": entityName, }) if err != nil { - t.Fatalf("failed to create an entity") + t.Fatalf("failed to create an entity:%v", err) } entityID := resp.Data["id"].(string) @@ -44,7 +44,7 @@ func createEntityAndAlias(client *api.Client, mountAccessor, entityName, aliasNa "mount_accessor": mountAccessor, }) if err != nil { - t.Fatalf("failed to create an entity alias") + t.Fatalf("failed to create an entity alias:%v", err) } return userClient, entityID } @@ -257,7 +257,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { t.Fatalf("failed to find the mfaConstrains") } if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 { - t.Fatalf("") + t.Fatalf("expected to see the methodID is enforced in MFAConstaint.Any") } for _, mfaAny := range mfaConstraints.Any { if mfaAny.ID != methodID || mfaAny.Type != "totp" || !mfaAny.UsesPasscode { diff --git a/vault/login_mfa.go b/vault/login_mfa.go index 599ff8af412d..6cb039023ef7 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -2087,8 +2087,8 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec return fmt.Errorf("error adding code to used cache: %w", err) } - // resetting the number of attempts to 0 after a successful validation - usedCodes.Set(rateLimitID, uint32(0), passcodeTTL) + // deleting the cache entry after a successful MFA validation + usedCodes.Delete(rateLimitID) return nil } From f8bd54a92cbc8cce1f732c3844709119faf26fc3 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Thu, 14 Apr 2022 05:28:05 -0700 Subject: [PATCH 11/11] checking for non-nil reference --- vault/core.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vault/core.go b/vault/core.go index 737358d2c808..169cecc0dfb1 100644 --- a/vault/core.go +++ b/vault/core.go @@ -2270,7 +2270,7 @@ func (c *Core) postUnseal(ctx context.Context, ctxCancelFunc context.CancelFunc, c.logger.Warn("disabling entities for local auth mounts through env var", "env", EnvVaultDisableLocalAuthMountEntities) } c.loginMFABackend.usedCodes = cache.New(0, 30*time.Second) - if c.systemBackend.mfaBackend != nil { + if c.systemBackend != nil && c.systemBackend.mfaBackend != nil { c.systemBackend.mfaBackend.usedCodes = cache.New(0, 30*time.Second) } c.logger.Info("post-unseal setup complete") @@ -2349,7 +2349,7 @@ func (c *Core) preSeal() error { } c.loginMFABackend.usedCodes = nil - if c.systemBackend.mfaBackend != nil { + if c.systemBackend != nil && c.systemBackend.mfaBackend != nil { c.systemBackend.mfaBackend.usedCodes = nil } preSealPhysical(c)