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:"-"`
// @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 (

View File

@ -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

View File

@ -81,6 +81,12 @@ 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. 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
// forwardToActive to trigger forwarding if a perf standby encounters
// 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.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")
return nil
}
@ -2340,6 +2349,9 @@ func (c *Core) preSeal() error {
}
c.loginMFABackend.usedCodes = nil
if c.systemBackend != nil && c.systemBackend.mfaBackend != nil {
c.systemBackend.mfaBackend.usedCodes = nil
}
preSealPhysical(c)
c.logger.Info("pre-seal teardown complete")

View File

@ -16,6 +16,83 @@ 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:%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) {
var noop *vault.NoopAudit
@ -67,15 +144,7 @@ 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().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()
auths, err := client.Sys().ListAuthWithContext(context.Background())
if err != nil {
t.Fatalf("bb")
}
@ -84,63 +153,24 @@ 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().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")
}
}
// 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
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) {
@ -152,176 +182,191 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
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
_, err = client.Logical().Write("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},
_, err = client.Logical().WriteWithContext(context.Background(), "identity/mfa/login-enforcement/randomName", map[string]interface{}{
"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().Write("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().Write("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().Write("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().Read("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().Write("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.ClientToken == "" {
t.Fatalf("successful mfa validation did not return a client token")
}
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 = user2Client.Logical().Write("auth/userpass/login/testuser", 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 = user2Client.Logical().Write("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 succeeded with an already used passcode")
}
if !strings.Contains(err.Error(), "code already used") {
t.Fatalf("expected error message to mention code already used")
}
// 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{}{
"entity_id": entityID,
"method_id": methodID,
})
if err != nil {
t.Fatalf("failed to destroy the MFA secret: %s", 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("expected to see the methodID is enforced in MFAConstaint.Any")
}
for _, mfaAny := range mfaConstraints.Any {
if mfaAny.ID != methodID || mfaAny.Type != "totp" || !mfaAny.UsesPasscode {
t.Fatalf("Invalid mfa constraints")
}
}
// 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)
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)
}
if secret.Auth == nil || secret.Auth.ClientToken == "" {
t.Fatalf("successful mfa validation did not return a client token")
}
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")
}
// 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
_, 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)
}
}

View File

@ -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",

View File

@ -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.`,

View File

@ -1184,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
@ -1276,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,
@ -1425,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)
return c.validateTOTP(ctx, mfaCreds, entityMFASecret, mConfig.ID, entity.ID, c.loginMFABackend.usedCodes, mConfig.GetTOTPConfig().MaxValidationAttempts)
case mfaMethodTypeOkta:
return c.validateOkta(ctx, mConfig, finalUsername)
@ -1997,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) 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")
}
@ -2013,11 +2023,35 @@ 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)
}
// 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)
if err != nil {
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))
// 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)
}
// deleting the cache entry after a successful MFA validation
usedCodes.Delete(rateLimitID)
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.
- `max_validation_attempts` `(int: 5)` - The maximum number of consecutive failed validation attempts.
### Sample Payload
```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.
- `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
}
}
```

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)
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 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,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.
### 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.