VAULT-5422: Add rate limit for TOTP passcode attempts (#14864)

* VAULT-5422: Add rate limit for TOTP passcode attempts

* fixing the docs

* CL

* feedback

* Additional info in doc

* rate limit is done per entity per methodID

* refactoring a test

* rate limit OSS work for policy MFA

* adding max_validation_attempts to TOTP config

* feedback

* checking for non-nil reference
This commit is contained in:
Hamid Ghaf 2022-04-14 13:48:24 -04:00 committed by GitHub
parent 3e6665f65d
commit a1d73ddfec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 447 additions and 317 deletions

3
changelog/14864.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
auth: enforce a rate limit for TOTP passcode validation attempts
```

View File

@ -212,6 +212,8 @@ type TOTPConfig struct {
KeySize uint32 `protobuf:"varint,6,opt,name=key_size,json=keySize,proto3" json:"key_size,omitempty" sentinel:"-"` KeySize uint32 `protobuf:"varint,6,opt,name=key_size,json=keySize,proto3" json:"key_size,omitempty" sentinel:"-"`
// @inject_tag: sentinel:"-" // @inject_tag: sentinel:"-"
QRSize int32 `protobuf:"varint,7,opt,name=qr_size,json=qrSize,proto3" json:"qr_size,omitempty" 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() { func (x *TOTPConfig) Reset() {
@ -295,6 +297,13 @@ func (x *TOTPConfig) GetQRSize() int32 {
return 0 return 0
} }
func (x *TOTPConfig) GetMaxValidationAttempts() uint32 {
if x != nil {
return x.MaxValidationAttempts
}
return 0
}
// DuoConfig represents the configuration information required to perform // DuoConfig represents the configuration information required to perform
// Duo authentication. // Duo authentication.
type DuoConfig struct { 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 0x71, 0x72, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x6d, 0x61, 0x78, 0x5f, 0x76, 0x61,
0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74,
0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x15, 0x6d, 0x61, 0x78, 0x56, 0x61, 0x6c, 0x69,
0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x65, 0x6d, 0x70, 0x74, 0x73, 0x22, 0xb6,
0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f,
0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18,
0x61, 0x70, 0x69, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69,
0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f,
0x1b, 0x0a, 0x09, 0x70, 0x75, 0x73, 0x68, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65,
0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x5f, 0x68, 0x6f, 0x73, 0x74,
0x75, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x48,
0x28, 0x08, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x50, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x73, 0x68, 0x5f,
0xa4, 0x01, 0x0a, 0x0a, 0x4f, 0x6b, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x73, 0x68,
0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73,
0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x50,
0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x0a, 0x4f, 0x6b, 0x74, 0x61,
0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d,
0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02,
0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e,
0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x65, 0x6d, 0x61, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x28, 0x08, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19,
0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xef, 0x01, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x49, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, 0x5f, 0x62, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69,
0x61, 0x73, 0x65, 0x36, 0x34, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08,
0x0c, 0x75, 0x73, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xef,
0x0d, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x01, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x49, 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12,
0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x36, 0x34, 0x5f, 0x6b, 0x65,
0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x42, 0x61, 0x73, 0x65,
0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x70, 0x5f, 0x36, 0x34, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x69, 0x67,
0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x64, 0x70, 0x55, 0x72, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73,
0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x05, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x72, 0x67, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1b, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x70, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x09, 0x52, 0x06, 0x69, 0x64, 0x70, 0x55, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67,
0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x72,
0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x67, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f,
0x63, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x72, 0x6c, 0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e,
0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x55, 0x72, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4e, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10,
0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x72, 0x6c,
0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x54, 0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65,
0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74,
0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65,
0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54,
0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65,
0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16, 0x0a, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12,
0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05, 0x20, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72,
0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f,
0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18,
0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a,
0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65,
0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d, 0x46, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c,
0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01,
0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12,
0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65,
0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d, 0x46, 0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65,
0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21,
0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73, 0x12, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02,
0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49,
0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f,
0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65,
0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f,
0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73,
0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68,
0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61,
0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64, 0x65, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73,
0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68,
0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74,
0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20,
0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e, 0x0a, 0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f,
0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x42, 0x30, 0x5a, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03,
0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69,
0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61, 0x62, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 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 ( var (

View File

@ -50,6 +50,8 @@ message TOTPConfig {
uint32 key_size = 6; uint32 key_size = 6;
// @inject_tag: sentinel:"-" // @inject_tag: sentinel:"-"
int32 qr_size = 7; int32 qr_size = 7;
// @inject_tag: sentinel:"-"
uint32 max_validation_attempts = 8;
} }
// DuoConfig represents the configuration information required to perform // DuoConfig represents the configuration information required to perform

View File

@ -81,6 +81,12 @@ const (
// MfaAuthResponse when the value is not specified in the server config // MfaAuthResponse when the value is not specified in the server config
defaultMFAAuthResponseTTL = 300 * time.Second defaultMFAAuthResponseTTL = 300 * time.Second
// defaultMaxTOTPValidateAttempts is the default value for the number
// 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 // ForwardSSCTokenToActive is the value that must be set in the
// forwardToActive to trigger forwarding if a perf standby encounters // forwardToActive to trigger forwarding if a perf standby encounters
// an SSC Token that it does not have the WAL state for. // an SSC Token that it does not have the WAL state for.
@ -2264,6 +2270,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.logger.Warn("disabling entities for local auth mounts through env var", "env", EnvVaultDisableLocalAuthMountEntities)
} }
c.loginMFABackend.usedCodes = cache.New(0, 30*time.Second) c.loginMFABackend.usedCodes = cache.New(0, 30*time.Second)
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") c.logger.Info("post-unseal setup complete")
return nil return nil
} }
@ -2340,6 +2349,9 @@ func (c *Core) preSeal() error {
} }
c.loginMFABackend.usedCodes = nil c.loginMFABackend.usedCodes = nil
if c.systemBackend != nil && c.systemBackend.mfaBackend != nil {
c.systemBackend.mfaBackend.usedCodes = nil
}
preSealPhysical(c) preSealPhysical(c)
c.logger.Info("pre-seal teardown complete") c.logger.Info("pre-seal teardown complete")

View File

@ -16,6 +16,83 @@ import (
"github.com/hashicorp/vault/vault" "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:%v", err)
}
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:%v", err)
}
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:%v", err)
}
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 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) { func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
var noop *vault.NoopAudit var noop *vault.NoopAudit
@ -67,15 +144,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
t.Fatalf("failed to enable userpass auth: %v", err) t.Fatalf("failed to enable userpass auth: %v", err)
} }
// Creating a user in the userpass auth mount auths, err := client.Sys().ListAuthWithContext(context.Background())
_, err = client.Logical().Write("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().ListAuth()
if err != nil { if err != nil {
t.Fatalf("bb") t.Fatalf("bb")
} }
@ -84,52 +153,12 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
mountAccessor = auths["userpass/"].Accessor mountAccessor = auths["userpass/"].Accessor
} }
userClient, err := client.Clone() // Creating two users in the userpass auth mount
if err != nil { userClient1, entityID1 := createEntityAndAlias(client, mountAccessor, "entity1", "testuser1", t)
t.Fatalf("failed to clone the client") userClient2, entityID2 := createEntityAndAlias(client, mountAccessor, "entity2", "testuser2", t)
}
userClient.SetToken(client.Token())
var entityID string
var groupID string
{
resp, err := userClient.Logical().Write("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().Write("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().Write("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")
}
}
// configure TOTP secret engine // configure TOTP secret engine
var totpPasscode string
var methodID string var methodID string
var userpassToken string
// login MFA // login MFA
{ {
// create a config // create a config
@ -141,6 +170,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
"skew": 1, "skew": 1,
"key_size": 10, "key_size": 10,
"qr_size": 100, "qr_size": 100,
"max_validation_attempts": 3,
}) })
if err != nil || (resp1 == nil) { if err != nil || (resp1 == nil) {
@ -152,54 +182,41 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
t.Fatalf("method ID is empty") t.Fatalf("method ID is empty")
} }
secret, err := client.Logical().Write(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().Write("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().Read("totp/code/loginMFA")
if err != nil {
t.Fatalf("failed to create totp passcode: %v", err)
}
totpPasscode = secret.Data["code"].(string)
// creating MFAEnforcementConfig // creating MFAEnforcementConfig
_, err = client.Logical().Write("identity/mfa/login-enforcement/randomName", map[string]interface{}{ _, err = client.Logical().WriteWithContext(context.Background(), "identity/mfa/login-enforcement/randomName", map[string]interface{}{
"auth_method_accessors": []string{mountAccessor},
"auth_method_types": []string{"userpass"}, "auth_method_types": []string{"userpass"},
"identity_group_ids": []string{groupID},
"identity_entity_ids": []string{entityID},
"name": "randomName", "name": "randomName",
"mfa_method_ids": []string{methodID}, "mfa_method_ids": []string{methodID},
}) })
if err != nil { if err != nil {
t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) t.Fatalf("failed to configure MFAEnforcementConfig: %v", err)
} }
}
// registering EntityIDs in the TOTP secret Engine for MethodID
totpEngineConfigName1 := registerEntityInTOTPEngine(client, entityID1, methodID, t)
totpEngineConfigName2 := registerEntityInTOTPEngine(client, entityID2, methodID, t)
// MFA single-phase login // MFA single-phase login
userClient.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", methodID, totpPasscode)) totpCodePath1 := fmt.Sprintf("totp/code/%s", totpEngineConfigName1)
secret, err = userClient.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{ 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)
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", "password": "testpassword",
}) })
if err != nil { if err != nil {
t.Fatalf("MFA failed: %v", err) t.Fatalf("MFA failed: %v", err)
} }
userpassToken = secret.Auth.ClientToken userpassToken := secret.Auth.ClientToken
userClient.SetToken(client.Token()) userClient1.SetToken(client.Token())
secret, err = userClient.Logical().Write("auth/token/lookup", map[string]interface{}{ secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/token/lookup", map[string]interface{}{
"token": userpassToken, "token": userpassToken,
}) })
if err != nil { if err != nil {
@ -207,19 +224,15 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
} }
entityIDCheck := secret.Data["entity_id"].(string) entityIDCheck := secret.Data["entity_id"].(string)
if entityIDCheck != entityID { if entityIDCheck != entityID1 {
t.Fatalf("different entityID assigned") t.Fatalf("different entityID assigned")
} }
// Two-phase login // Two-phase login
user2Client, err := client.Clone() headers := userClient1.Headers()
if err != nil {
t.Fatalf("failed to clone the client")
}
headers := user2Client.Headers()
headers.Del("X-Vault-MFA") headers.Del("X-Vault-MFA")
user2Client.SetHeaders(headers) userClient1.SetHeaders(headers)
secret, err = user2Client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{ secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{
"password": "testpassword", "password": "testpassword",
}) })
if err != nil { if err != nil {
@ -244,7 +257,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
t.Fatalf("failed to find the mfaConstrains") t.Fatalf("failed to find the mfaConstrains")
} }
if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 { 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 { for _, mfaAny := range mfaConstraints.Any {
if mfaAny.ID != methodID || mfaAny.Type != "totp" || !mfaAny.UsesPasscode { if mfaAny.ID != methodID || mfaAny.Type != "totp" || !mfaAny.UsesPasscode {
@ -256,16 +269,16 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
// waiting for 5 seconds so that a fresh code could be generated // waiting for 5 seconds so that a fresh code could be generated
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
// getting a fresh totp passcode for the validation step // getting a fresh totp passcode for the validation step
totpResp, err := client.Logical().Read("totp/code/loginMFA") totpResp, err := client.Logical().ReadWithContext(context.Background(), totpCodePath1)
if err != nil { if err != nil {
t.Fatalf("failed to create totp passcode: %v", err) t.Fatalf("failed to create totp passcode: %v", err)
} }
totpPasscode = totpResp.Data["code"].(string) totpPasscode1 = totpResp.Data["code"].(string)
secret, err = user2Client.Logical().Write("sys/mfa/validate", map[string]interface{}{ secret, err = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{
"mfa_request_id": secret.Auth.MFARequirement.MFARequestID, "mfa_request_id": secret.Auth.MFARequirement.MFARequestID,
"mfa_payload": map[string][]string{ "mfa_payload": map[string][]string{
methodID: {totpPasscode}, methodID: {totpPasscode1},
}, },
}) })
if err != nil { if err != nil {
@ -291,7 +304,7 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
} }
// check for login request expiration // check for login request expiration
secret, err = user2Client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{ secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{
"password": "testpassword", "password": "testpassword",
}) })
if err != nil { if err != nil {
@ -302,10 +315,10 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
t.Fatalf("two phase login returned nil MFARequirement") t.Fatalf("two phase login returned nil MFARequirement")
} }
_, err = user2Client.Logical().Write("sys/mfa/validate", map[string]interface{}{ _, err = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{
"mfa_request_id": secret.Auth.MFARequirement.MFARequestID, "mfa_request_id": secret.Auth.MFARequirement.MFARequestID,
"mfa_payload": map[string][]string{ "mfa_payload": map[string][]string{
methodID: {totpPasscode}, methodID: {totpPasscode1},
}, },
}) })
if err == nil { if err == nil {
@ -315,13 +328,45 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
t.Fatalf("expected error message to mention code already used") t.Fatalf("expected error message to mention code already used")
} }
// 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 < 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{
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 4 exceeded the allowed attempts 3") {
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)
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 // Destroy the secret so that the token can self generate
_, err = userClient.Logical().Write(fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ _, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{
"entity_id": entityID, "entity_id": entityID1,
"method_id": methodID, "method_id": methodID,
}) })
if err != nil { if err != nil {
t.Fatalf("failed to destroy the MFA secret: %s", err) t.Fatalf("failed to destroy the MFA secret: %s", err)
} }
} }
}

View File

@ -61,6 +61,7 @@ func TestLoginMFA_Method_CRUD(t *testing.T) {
"skew": 1, "skew": 1,
"key_size": uint(10), "key_size": uint(10),
"qr_size": 100, "qr_size": 100,
"max_validation_attempts": 1,
}, },
"issuer", "issuer",
"zCorp", "zCorp",

View File

@ -148,6 +148,10 @@ func mfaPaths(i *IdentityStore) []*framework.Path {
Type: framework.TypeString, Type: framework.TypeString,
Description: `The unique identifier for this MFA method.`, Description: `The unique identifier for this MFA method.`,
}, },
"max_validation_attempts": {
Type: framework.TypeInt,
Description: `Max number of allowed validation attempts.`,
},
"issuer": { "issuer": {
Type: framework.TypeString, Type: framework.TypeString,
Description: `The name of the key's issuing organization.`, Description: `The name of the key's issuing organization.`,

View File

@ -1184,6 +1184,7 @@ func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config) (map[string]interface{}
respData["key_size"] = totpConfig.KeySize respData["key_size"] = totpConfig.KeySize
respData["qr_size"] = totpConfig.QRSize respData["qr_size"] = totpConfig.QRSize
respData["algorithm"] = otplib.Algorithm(totpConfig.Algorithm).String() respData["algorithm"] = otplib.Algorithm(totpConfig.Algorithm).String()
respData["max_validation_attempts"] = totpConfig.MaxValidationAttempts
case *mfa.Config_OktaConfig: case *mfa.Config_OktaConfig:
oktaConfig := mConfig.GetOktaConfig() oktaConfig := mConfig.GetOktaConfig()
respData["org_name"] = oktaConfig.OrgName respData["org_name"] = oktaConfig.OrgName
@ -1276,6 +1277,14 @@ func parseTOTPConfig(mConfig *mfa.Config, d *framework.FieldData) error {
return fmt.Errorf("issuer must be set") 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{ config := &mfa.TOTPConfig{
Issuer: issuer, Issuer: issuer,
Period: uint32(period), Period: uint32(period),
@ -1284,6 +1293,7 @@ func parseTOTPConfig(mConfig *mfa.Config, d *framework.FieldData) error {
Skew: uint32(skew), Skew: uint32(skew),
KeySize: uint32(keySize), KeySize: uint32(keySize),
QRSize: int32(d.Get("qr_size").(int)), QRSize: int32(d.Get("qr_size").(int)),
MaxValidationAttempts: uint32(maxValidationAttempt),
} }
mConfig.Config = &mfa.Config_TOTPConfig{ mConfig.Config = &mfa.Config_TOTPConfig{
TOTPConfig: config, TOTPConfig: config,
@ -1425,7 +1435,7 @@ func (c *Core) validateLoginMFAInternal(ctx context.Context, methodID string, en
return fmt.Errorf("MFA credentials not supplied") 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, mConfig.GetTOTPConfig().MaxValidationAttempts)
case mfaMethodTypeOkta: case mfaMethodTypeOkta:
return c.validateOkta(ctx, mConfig, finalUsername) return c.validateOkta(ctx, mConfig, finalUsername)
@ -1997,7 +2007,7 @@ func (c *Core) validatePingID(ctx context.Context, mConfig *mfa.Config, username
return nil 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 uint32) error {
if len(creds) == 0 { if len(creds) == 0 {
return fmt.Errorf("missing TOTP passcode") return fmt.Errorf("missing TOTP passcode")
} }
@ -2013,11 +2023,35 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec
usedName := fmt.Sprintf("%s_%s", configID, creds[0]) usedName := fmt.Sprintf("%s_%s", configID, creds[0])
_, ok := c.loginMFABackend.usedCodes.Get(usedName) _, ok := usedCodes.Get(usedName)
if ok { if ok {
return fmt.Errorf("code already used; new code is available in %v seconds", totpSecret.Period) 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))
// Enforcing rate limit per MethodID per EntityID
rateLimitID := fmt.Sprintf("%s_%s", configID, entityID)
numAttempts, _ := usedCodes.Get(rateLimitID)
if numAttempts == nil {
usedCodes.Set(rateLimitID, uint32(1), passcodeTTL)
} else {
num, ok := numAttempts.(uint32)
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. Please try again in %v seconds", num+1, maximumValidationAttempts, passcodeTTL)
}
err := usedCodes.Increment(rateLimitID, 1)
if err != nil {
return fmt.Errorf("failed to increment the TOTP code counter")
}
}
key, err := c.fetchTOTPKey(ctx, configID, entityID) key, err := c.fetchTOTPKey(ctx, configID, entityID)
if err != nil { if err != nil {
return errwrap.Wrapf("error fetching TOTP key: {{err}}", err) return errwrap.Wrapf("error fetching TOTP key: {{err}}", err)
@ -2048,11 +2082,14 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec
validityPeriod := time.Duration(int64(time.Second) * int64(totpSecret.Period) * int64(2+totpSecret.Skew)) validityPeriod := time.Duration(int64(time.Second) * int64(totpSecret.Period) * int64(2+totpSecret.Skew))
// Adding the used code to the cache // Adding the used code to the cache
err = c.loginMFABackend.usedCodes.Add(usedName, nil, validityPeriod) err = usedCodes.Add(usedName, nil, validityPeriod)
if err != nil { if err != nil {
return fmt.Errorf("error adding code to used cache: %w", err) return fmt.Errorf("error adding code to used cache: %w", err)
} }
// deleting the cache entry after a successful MFA validation
usedCodes.Delete(rateLimitID)
return nil return nil
} }

View File

@ -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. - `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 ### Sample Payload
```json ```json

View File

@ -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. - `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 ### Sample Payload
```json ```json
@ -88,6 +90,7 @@ $ curl \
"qr_size": 200, "qr_size": 200,
"skew": 1, "skew": 1,
"type": "totp" "type": "totp"
"max_validation_attempts": 5
} }
} }
``` ```

View File

@ -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) 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 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 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 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 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 MFA, an MFA method needs to be configured. Please see [Login MFA API](/api-docs/secret/identity/mfa) for details
@ -190,3 +190,12 @@ $ 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. 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
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_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.