diff --git a/changelog/14864.txt b/changelog/14864.txt new file mode 100644 index 000000000..34af10f1e --- /dev/null +++ b/changelog/14864.txt @@ -0,0 +1,3 @@ +```release-note:improvement +auth: enforce a rate limit for TOTP passcode validation attempts +``` diff --git a/helper/identity/mfa/types.pb.go b/helper/identity/mfa/types.pb.go index f82ccb46b..789def20f 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 c20386cb9..decade25b 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 5a15145ac..169cecc0d 100644 --- a/vault/core.go +++ b/vault/core.go @@ -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") diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index 19869bdb7..9103bc845 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -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) + } } diff --git a/vault/external_tests/mfa/login_mfa_test.go b/vault/external_tests/mfa/login_mfa_test.go index cd8dfd684..8a971ea63 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 bd0d11116..c434bf9dc 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 45092ef2a..6cb039023 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -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 } diff --git a/website/content/api-docs/secret/identity/mfa/totp.mdx b/website/content/api-docs/secret/identity/mfa/totp.mdx index bfd255bf2..e170fa474 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 ff43832b2..092f27cea 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 207ec836d..d475cedc0 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 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.