From 874c12de46791c988d9993e076b98198bed379b6 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Fri, 11 Feb 2022 12:03:02 -0800 Subject: [PATCH 01/17] Login MFA --- api/client.go | 6 + api/secret.go | 3 + helper/forwarding/types.pb.go | 2 +- helper/identity/mfa/mfa.go | 19 + helper/identity/mfa/types.pb.go | 316 +- helper/identity/mfa/types.proto | 15 + helper/identity/types.pb.go | 2 +- helper/namespace/namespace_test.go | 99 +- helper/storagepacker/types.pb.go | 2 +- http/handler.go | 2 +- http/logical_test.go | 19 +- physical/raft/types.pb.go | 2 +- sdk/database/dbplugin/database.pb.go | 2 +- sdk/database/dbplugin/v5/proto/database.pb.go | 2 +- sdk/logical/auth.go | 3 + sdk/logical/identity.pb.go | 280 +- sdk/logical/identity.proto | 17 +- sdk/logical/plugin.pb.go | 2 +- sdk/logical/translate_response.go | 2 + sdk/plugin/pb/backend.pb.go | 2 +- vault/activity/activity_log.pb.go | 2 +- vault/core.go | 70 +- vault/core_util.go | 4 - .../identity/login_mfa_duo_test.go | 290 ++ .../identity/login_mfa_okta_test.go | 354 +++ .../identity/login_mfa_totp_test.go | 886 ++++++ vault/external_tests/mfa/login_mfa_test.go | 1382 ++++++++ vault/identity_store.go | 347 +- vault/identity_store_structs.go | 1 + vault/logical_system.go | 25 +- vault/logical_system_helpers.go | 6 +- vault/login_mfa.go | 2798 +++++++++++++++++ vault/mfa_auth_resp_priority_queue.go | 100 + vault/mfa_auth_resp_priority_queue_test.go | 109 + vault/request_forwarding_service.pb.go | 2 +- vault/request_handling.go | 290 +- 36 files changed, 7234 insertions(+), 229 deletions(-) create mode 100644 vault/external_tests/identity/login_mfa_duo_test.go create mode 100644 vault/external_tests/identity/login_mfa_okta_test.go create mode 100644 vault/external_tests/identity/login_mfa_totp_test.go create mode 100644 vault/external_tests/mfa/login_mfa_test.go create mode 100644 vault/login_mfa.go create mode 100644 vault/mfa_auth_resp_priority_queue.go create mode 100644 vault/mfa_auth_resp_priority_queue_test.go diff --git a/api/client.go b/api/client.go index 9f84dd83d6fd..6050999b1244 100644 --- a/api/client.go +++ b/api/client.go @@ -798,6 +798,12 @@ func (c *Client) setNamespace(namespace string) { c.headers.Set(consts.NamespaceHeaderName, namespace) } +func (c *Client) ClearNamespace() { + c.modifyLock.Lock() + defer c.modifyLock.Unlock() + c.headers.Del(consts.NamespaceHeaderName) +} + // Token returns the access token being used by this client. It will // return the empty string if there is no token set. func (c *Client) Token() string { diff --git a/api/secret.go b/api/secret.go index 64865d0ba1dc..a3a288bf142d 100644 --- a/api/secret.go +++ b/api/secret.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/sdk/helper/jsonutil" + "github.com/hashicorp/vault/sdk/logical" ) // Secret is the structure returned for every secret within Vault. @@ -297,6 +298,8 @@ type SecretAuth struct { LeaseDuration int `json:"lease_duration"` Renewable bool `json:"renewable"` + + MFARequirement *logical.MFARequirement `json:"mfa_requirement"` } // ParseSecret is used to parse a secret value from JSON from an io.Reader. diff --git a/helper/forwarding/types.pb.go b/helper/forwarding/types.pb.go index b7ffa70569e0..94400fc8a4c9 100644 --- a/helper/forwarding/types.pb.go +++ b/helper/forwarding/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: helper/forwarding/types.proto package forwarding diff --git a/helper/identity/mfa/mfa.go b/helper/identity/mfa/mfa.go index 8f513f9050fe..d4bbf10b4846 100644 --- a/helper/identity/mfa/mfa.go +++ b/helper/identity/mfa/mfa.go @@ -24,3 +24,22 @@ func (c *Config) Clone() (*Config, error) { return &clonedConfig, nil } + +func (c *MFAEnforcementConfig) Clone() (*MFAEnforcementConfig, error) { + if c == nil { + return nil, fmt.Errorf("nil config") + } + + marshaledConfig, err := proto.Marshal(c) + if err != nil { + return nil, fmt.Errorf("failed to marshal config: %w", err) + } + + var clonedConfig MFAEnforcementConfig + err = proto.Unmarshal(marshaledConfig, &clonedConfig) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return &clonedConfig, nil +} diff --git a/helper/identity/mfa/types.pb.go b/helper/identity/mfa/types.pb.go index 5e5bf2a854e1..62f283e79025 100644 --- a/helper/identity/mfa/types.pb.go +++ b/helper/identity/mfa/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: helper/identity/mfa/types.proto package mfa @@ -47,6 +47,8 @@ type Config struct { // *Config_DuoConfig // *Config_PingIDConfig Config isConfig_Config `protobuf_oneof:"config" sentinel:"-"` + // @inject_tag: sentinel:"-" + NamespaceID string `protobuf:"bytes,10,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty" sentinel:"-"` } func (x *Config) Reset() { @@ -151,6 +153,13 @@ func (x *Config) GetPingIDConfig() *PingIDConfig { return nil } +func (x *Config) GetNamespaceID() string { + if x != nil { + return x.NamespaceID + } + return "" +} + type isConfig_Config interface { isConfig_Config() } @@ -745,12 +754,117 @@ func (x *TOTPSecret) GetKey() string { return "" } +// MFAEnforcementConfig is what the user provides to the +// mfa/login_enforcement endpoint. +type MFAEnforcementConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + NamespaceID string `protobuf:"bytes,2,opt,name=namespace_id,json=namespaceID,proto3" json:"namespace_id,omitempty"` + MFAMethodIDs []string `protobuf:"bytes,3,rep,name=mfa_method_ids,json=mfaMethodIds,proto3" json:"mfa_method_ids,omitempty"` + AuthMethodAccessors []string `protobuf:"bytes,4,rep,name=auth_method_accessors,json=authMethodAccessors,proto3" json:"auth_method_accessors,omitempty"` + AuthMethodTypes []string `protobuf:"bytes,5,rep,name=auth_method_types,json=authMethodTypes,proto3" json:"auth_method_types,omitempty"` + IdentityGroupIds []string `protobuf:"bytes,6,rep,name=identity_group_ids,json=identityGroupIds,proto3" json:"identity_group_ids,omitempty"` + IdentityEntityIDs []string `protobuf:"bytes,7,rep,name=identity_entity_ids,json=identityEntityIds,proto3" json:"identity_entity_ids,omitempty"` + ID string `protobuf:"bytes,8,opt,name=id,proto3" json:"id,omitempty"` +} + +func (x *MFAEnforcementConfig) Reset() { + *x = MFAEnforcementConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_helper_identity_mfa_types_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MFAEnforcementConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MFAEnforcementConfig) ProtoMessage() {} + +func (x *MFAEnforcementConfig) ProtoReflect() protoreflect.Message { + mi := &file_helper_identity_mfa_types_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MFAEnforcementConfig.ProtoReflect.Descriptor instead. +func (*MFAEnforcementConfig) Descriptor() ([]byte, []int) { + return file_helper_identity_mfa_types_proto_rawDescGZIP(), []int{7} +} + +func (x *MFAEnforcementConfig) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *MFAEnforcementConfig) GetNamespaceID() string { + if x != nil { + return x.NamespaceID + } + return "" +} + +func (x *MFAEnforcementConfig) GetMFAMethodIDs() []string { + if x != nil { + return x.MFAMethodIDs + } + return nil +} + +func (x *MFAEnforcementConfig) GetAuthMethodAccessors() []string { + if x != nil { + return x.AuthMethodAccessors + } + return nil +} + +func (x *MFAEnforcementConfig) GetAuthMethodTypes() []string { + if x != nil { + return x.AuthMethodTypes + } + return nil +} + +func (x *MFAEnforcementConfig) GetIdentityGroupIds() []string { + if x != nil { + return x.IdentityGroupIds + } + return nil +} + +func (x *MFAEnforcementConfig) GetIdentityEntityIDs() []string { + if x != nil { + return x.IdentityEntityIDs + } + return nil +} + +func (x *MFAEnforcementConfig) GetID() string { + if x != nil { + return x.ID + } + return "" +} + var File_helper_identity_mfa_types_proto protoreflect.FileDescriptor var file_helper_identity_mfa_types_proto_rawDesc = []byte{ 0x0a, 0x1f, 0x68, 0x65, 0x6c, 0x70, 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x03, 0x6d, 0x66, 0x61, 0x22, 0xed, 0x02, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x6f, 0x12, 0x03, 0x6d, 0x66, 0x61, 0x22, 0x90, 0x03, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, @@ -772,78 +886,101 @@ var file_helper_identity_mfa_types_proto_rawDesc = []byte{ 0x69, 0x67, 0x12, 0x38, 0x0a, 0x0d, 0x70, 0x69, 0x6e, 0x67, 0x69, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x49, 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x0c, - 0x70, 0x69, 0x6e, 0x67, 0x69, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x08, 0x0a, 0x06, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xba, 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, 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, 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, 0x93, 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, 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, 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, + 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, + 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, + 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, 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, 0x93, 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, 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 ( @@ -858,15 +995,16 @@ func file_helper_identity_mfa_types_proto_rawDescGZIP() []byte { return file_helper_identity_mfa_types_proto_rawDescData } -var file_helper_identity_mfa_types_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_helper_identity_mfa_types_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_helper_identity_mfa_types_proto_goTypes = []interface{}{ - (*Config)(nil), // 0: mfa.Config - (*TOTPConfig)(nil), // 1: mfa.TOTPConfig - (*DuoConfig)(nil), // 2: mfa.DuoConfig - (*OktaConfig)(nil), // 3: mfa.OktaConfig - (*PingIDConfig)(nil), // 4: mfa.PingIDConfig - (*Secret)(nil), // 5: mfa.Secret - (*TOTPSecret)(nil), // 6: mfa.TOTPSecret + (*Config)(nil), // 0: mfa.Config + (*TOTPConfig)(nil), // 1: mfa.TOTPConfig + (*DuoConfig)(nil), // 2: mfa.DuoConfig + (*OktaConfig)(nil), // 3: mfa.OktaConfig + (*PingIDConfig)(nil), // 4: mfa.PingIDConfig + (*Secret)(nil), // 5: mfa.Secret + (*TOTPSecret)(nil), // 6: mfa.TOTPSecret + (*MFAEnforcementConfig)(nil), // 7: mfa.MFAEnforcementConfig } var file_helper_identity_mfa_types_proto_depIDxs = []int32{ 1, // 0: mfa.Config.totp_config:type_name -> mfa.TOTPConfig @@ -971,6 +1109,18 @@ func file_helper_identity_mfa_types_proto_init() { return nil } } + file_helper_identity_mfa_types_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MFAEnforcementConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_helper_identity_mfa_types_proto_msgTypes[0].OneofWrappers = []interface{}{ (*Config_TOTPConfig)(nil), @@ -987,7 +1137,7 @@ func file_helper_identity_mfa_types_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_helper_identity_mfa_types_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/helper/identity/mfa/types.proto b/helper/identity/mfa/types.proto index 8358a6f5d18d..914eda6cf70a 100644 --- a/helper/identity/mfa/types.proto +++ b/helper/identity/mfa/types.proto @@ -26,6 +26,8 @@ message Config { DuoConfig duo_config = 8; PingIDConfig pingid_config = 9; } + // @inject_tag: sentinel:"-" + string namespace_id = 10; } // TOTPConfig represents the configuration information required to generate @@ -129,3 +131,16 @@ message TOTPSecret { // @inject_tag: sentinel:"-" string key = 9; } + +// MFAEnforcementConfig is what the user provides to the +// mfa/login_enforcement endpoint. +message MFAEnforcementConfig { + string name = 1; + string namespace_id = 2; + repeated string mfa_method_ids = 3; + repeated string auth_method_accessors = 4; + repeated string auth_method_types = 5; + repeated string identity_group_ids = 6; + repeated string identity_entity_ids = 7; + string id = 8; +} diff --git a/helper/identity/types.pb.go b/helper/identity/types.pb.go index c4730c775b89..81b29b29ed4b 100644 --- a/helper/identity/types.pb.go +++ b/helper/identity/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: helper/identity/types.proto package identity diff --git a/helper/namespace/namespace_test.go b/helper/namespace/namespace_test.go index 7c0499f8ada2..442b46b90447 100644 --- a/helper/namespace/namespace_test.go +++ b/helper/namespace/namespace_test.go @@ -1,6 +1,8 @@ package namespace -import "testing" +import ( + "testing" +) func TestSplitIDFromString(t *testing.T) { tcases := []struct { @@ -72,3 +74,98 @@ func TestSplitIDFromString(t *testing.T) { } } } + +func TestHasParent(t *testing.T) { + // Create ns1 + ns1 := &Namespace{ + ID: "id1", + Path: "ns1/", + } + + // Create ns1/ns2 + ns2 := &Namespace{ + ID: "id2", + Path: "ns1/ns2/", + } + + // Create ns1/ns2/ns3 + ns3 := &Namespace{ + ID: "id3", + Path: "ns1/ns2/ns3/", + } + + // Create ns4 + ns4 := &Namespace{ + ID: "id4", + Path: "ns4/", + } + + // Create ns4/ns5 + ns5 := &Namespace{ + ID: "id5", + Path: "ns4/ns5/", + } + + tests := []struct { + name string + parent *Namespace + ns *Namespace + expected bool + }{ + { + "is root an ancestor of ns1", + RootNamespace, + ns1, + true, + }, + { + "is ns1 an ancestor of ns2", + ns1, + ns2, + true, + }, + { + "is ns2 an ancestor of ns3", + ns2, + ns3, + true, + }, + { + "is ns1 an ancestor of ns3", + ns1, + ns3, + true, + }, + { + "is root an ancestor of ns3", + RootNamespace, + ns3, + true, + }, + { + "is ns4 an ancestor of ns3", + ns4, + ns3, + false, + }, + { + "is ns5 an ancestor of ns3", + ns5, + ns3, + false, + }, + { + "is ns1 an ancestor of ns5", + ns1, + ns5, + false, + }, + } + + for _, test := range tests { + actual := test.ns.HasParent(test.parent) + if actual != test.expected { + t.Fatalf("bad ancestor calculation; name: %q, actual: %t, expected: %t", test.name, actual, test.expected) + } + } +} diff --git a/helper/storagepacker/types.pb.go b/helper/storagepacker/types.pb.go index 4c5b14edd404..670576acbb9f 100644 --- a/helper/storagepacker/types.pb.go +++ b/helper/storagepacker/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: helper/storagepacker/types.proto package storagepacker diff --git a/http/handler.go b/http/handler.go index 527397d5ea77..70904e8e1e04 100644 --- a/http/handler.go +++ b/http/handler.go @@ -1127,7 +1127,7 @@ func parseMFAHeader(req *logical.Request) error { shardSplits := strings.SplitN(mfaHeaderValue, ":", 2) if shardSplits[0] == "" { - return fmt.Errorf("invalid data in header %q; missing method name", MFAHeaderName) + return fmt.Errorf("invalid data in header %q; missing method name or ID", MFAHeaderName) } if shardSplits[1] == "" { diff --git a/http/logical_test.go b/http/logical_test.go index 0560fcdcebac..08e2dc152929 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -219,15 +219,16 @@ func TestLogical_CreateToken(t *testing.T) { "data": nil, "wrap_info": nil, "auth": map[string]interface{}{ - "policies": []interface{}{"root"}, - "token_policies": []interface{}{"root"}, - "metadata": nil, - "lease_duration": json.Number("0"), - "renewable": false, - "entity_id": "", - "token_type": "service", - "orphan": false, - "num_uses": json.Number("0"), + "policies": []interface{}{"root"}, + "token_policies": []interface{}{"root"}, + "metadata": nil, + "lease_duration": json.Number("0"), + "renewable": false, + "entity_id": "", + "token_type": "service", + "orphan": false, + "mfa_requirement": nil, + "num_uses": json.Number("0"), }, "warnings": nilWarnings, } diff --git a/physical/raft/types.pb.go b/physical/raft/types.pb.go index 98dc72982881..fbc03c7aeebf 100644 --- a/physical/raft/types.pb.go +++ b/physical/raft/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: physical/raft/types.proto package raft diff --git a/sdk/database/dbplugin/database.pb.go b/sdk/database/dbplugin/database.pb.go index 4e8b0098ffca..8af3951e68c9 100644 --- a/sdk/database/dbplugin/database.pb.go +++ b/sdk/database/dbplugin/database.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: sdk/database/dbplugin/database.proto package dbplugin diff --git a/sdk/database/dbplugin/v5/proto/database.pb.go b/sdk/database/dbplugin/v5/proto/database.pb.go index 4416e0acc40e..fbf0c1245fd3 100644 --- a/sdk/database/dbplugin/v5/proto/database.pb.go +++ b/sdk/database/dbplugin/v5/proto/database.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: sdk/database/dbplugin/v5/proto/database.proto package proto diff --git a/sdk/logical/auth.go b/sdk/logical/auth.go index 2bfb6e0015a1..7f68bc936e8b 100644 --- a/sdk/logical/auth.go +++ b/sdk/logical/auth.go @@ -100,6 +100,9 @@ type Auth struct { // Orphan is set if the token does not have a parent Orphan bool `json:"orphan"` + + // MFARequirement + MFARequirement *MFARequirement `json:"mfa_requirement"` } func (a *Auth) GoString() string { diff --git a/sdk/logical/identity.pb.go b/sdk/logical/identity.pb.go index b221ccc3b325..c472b68a099e 100644 --- a/sdk/logical/identity.pb.go +++ b/sdk/logical/identity.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: sdk/logical/identity.proto package logical @@ -310,6 +310,171 @@ func (x *Group) GetNamespaceID() string { return "" } +type MFAMethodID struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + ID string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + UsesPasscode bool `protobuf:"varint,3,opt,name=uses_passcode,json=usesPasscode,proto3" json:"uses_passcode,omitempty"` +} + +func (x *MFAMethodID) Reset() { + *x = MFAMethodID{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_logical_identity_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MFAMethodID) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MFAMethodID) ProtoMessage() {} + +func (x *MFAMethodID) ProtoReflect() protoreflect.Message { + mi := &file_sdk_logical_identity_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MFAMethodID.ProtoReflect.Descriptor instead. +func (*MFAMethodID) Descriptor() ([]byte, []int) { + return file_sdk_logical_identity_proto_rawDescGZIP(), []int{3} +} + +func (x *MFAMethodID) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *MFAMethodID) GetID() string { + if x != nil { + return x.ID + } + return "" +} + +func (x *MFAMethodID) GetUsesPasscode() bool { + if x != nil { + return x.UsesPasscode + } + return false +} + +type MFAConstraintAny struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Any []*MFAMethodID `protobuf:"bytes,1,rep,name=any,proto3" json:"any,omitempty"` +} + +func (x *MFAConstraintAny) Reset() { + *x = MFAConstraintAny{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_logical_identity_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MFAConstraintAny) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MFAConstraintAny) ProtoMessage() {} + +func (x *MFAConstraintAny) ProtoReflect() protoreflect.Message { + mi := &file_sdk_logical_identity_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MFAConstraintAny.ProtoReflect.Descriptor instead. +func (*MFAConstraintAny) Descriptor() ([]byte, []int) { + return file_sdk_logical_identity_proto_rawDescGZIP(), []int{4} +} + +func (x *MFAConstraintAny) GetAny() []*MFAMethodID { + if x != nil { + return x.Any + } + return nil +} + +type MFARequirement struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MFARequestID string `protobuf:"bytes,1,opt,name=mfa_request_id,json=mfaRequestId,proto3" json:"mfa_request_id,omitempty"` + MFAConstraints map[string]*MFAConstraintAny `protobuf:"bytes,2,rep,name=mfa_constraints,json=mfaConstraints,proto3" json:"mfa_constraints,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *MFARequirement) Reset() { + *x = MFARequirement{} + if protoimpl.UnsafeEnabled { + mi := &file_sdk_logical_identity_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MFARequirement) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MFARequirement) ProtoMessage() {} + +func (x *MFARequirement) ProtoReflect() protoreflect.Message { + mi := &file_sdk_logical_identity_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MFARequirement.ProtoReflect.Descriptor instead. +func (*MFARequirement) Descriptor() ([]byte, []int) { + return file_sdk_logical_identity_proto_rawDescGZIP(), []int{5} +} + +func (x *MFARequirement) GetMFARequestID() string { + if x != nil { + return x.MFARequestID + } + return "" +} + +func (x *MFARequirement) GetMFAConstraints() map[string]*MFAConstraintAny { + if x != nil { + return x.MFAConstraints + } + return nil +} + var File_sdk_logical_identity_proto protoreflect.FileDescriptor var file_sdk_logical_identity_proto_rawDesc = []byte{ @@ -372,10 +537,34 @@ var file_sdk_logical_identity_proto_rawDesc = []byte{ 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x42, 0x28, 0x5a, 0x26, 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, - 0x73, 0x64, 0x6b, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x01, 0x22, 0x56, 0x0a, 0x0b, 0x4d, 0x46, 0x41, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x44, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x75, 0x73, 0x65, 0x73, 0x5f, 0x70, 0x61, 0x73, + 0x73, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, 0x65, + 0x73, 0x50, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x3a, 0x0a, 0x10, 0x4d, 0x46, 0x41, + 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x41, 0x6e, 0x79, 0x12, 0x26, 0x0a, + 0x03, 0x61, 0x6e, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x6c, 0x6f, 0x67, + 0x69, 0x63, 0x61, 0x6c, 0x2e, 0x4d, 0x46, 0x41, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x44, + 0x52, 0x03, 0x61, 0x6e, 0x79, 0x22, 0xea, 0x01, 0x0a, 0x0e, 0x4d, 0x46, 0x41, 0x52, 0x65, 0x71, + 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, 0x5f, + 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x54, + 0x0a, 0x0f, 0x6d, 0x66, 0x61, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, + 0x6c, 0x2e, 0x4d, 0x46, 0x41, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x2e, 0x4d, 0x66, 0x61, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x6d, 0x66, 0x61, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, + 0x69, 0x6e, 0x74, 0x73, 0x1a, 0x5c, 0x0a, 0x13, 0x4d, 0x66, 0x61, 0x43, 0x6f, 0x6e, 0x73, 0x74, + 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2f, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6c, + 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x2e, 0x4d, 0x46, 0x41, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, + 0x61, 0x69, 0x6e, 0x74, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x42, 0x28, 0x5a, 0x26, 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, 0x73, 0x64, 0x6b, 0x2f, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -390,27 +579,34 @@ func file_sdk_logical_identity_proto_rawDescGZIP() []byte { return file_sdk_logical_identity_proto_rawDescData } -var file_sdk_logical_identity_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_sdk_logical_identity_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_sdk_logical_identity_proto_goTypes = []interface{}{ - (*Entity)(nil), // 0: logical.Entity - (*Alias)(nil), // 1: logical.Alias - (*Group)(nil), // 2: logical.Group - nil, // 3: logical.Entity.MetadataEntry - nil, // 4: logical.Alias.MetadataEntry - nil, // 5: logical.Alias.CustomMetadataEntry - nil, // 6: logical.Group.MetadataEntry + (*Entity)(nil), // 0: logical.Entity + (*Alias)(nil), // 1: logical.Alias + (*Group)(nil), // 2: logical.Group + (*MFAMethodID)(nil), // 3: logical.MFAMethodID + (*MFAConstraintAny)(nil), // 4: logical.MFAConstraintAny + (*MFARequirement)(nil), // 5: logical.MFARequirement + nil, // 6: logical.Entity.MetadataEntry + nil, // 7: logical.Alias.MetadataEntry + nil, // 8: logical.Alias.CustomMetadataEntry + nil, // 9: logical.Group.MetadataEntry + nil, // 10: logical.MFARequirement.MFAConstraintsEntry } var file_sdk_logical_identity_proto_depIDxs = []int32{ - 1, // 0: logical.Entity.aliases:type_name -> logical.Alias - 3, // 1: logical.Entity.metadata:type_name -> logical.Entity.MetadataEntry - 4, // 2: logical.Alias.metadata:type_name -> logical.Alias.MetadataEntry - 5, // 3: logical.Alias.custom_metadata:type_name -> logical.Alias.CustomMetadataEntry - 6, // 4: logical.Group.metadata:type_name -> logical.Group.MetadataEntry - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 1, // 0: logical.Entity.aliases:type_name -> logical.Alias + 6, // 1: logical.Entity.metadata:type_name -> logical.Entity.MetadataEntry + 7, // 2: logical.Alias.metadata:type_name -> logical.Alias.MetadataEntry + 8, // 3: logical.Alias.custom_metadata:type_name -> logical.Alias.CustomMetadataEntry + 9, // 4: logical.Group.metadata:type_name -> logical.Group.MetadataEntry + 3, // 5: logical.MFAConstraintAny.any:type_name -> logical.MFAMethodID + 10, // 6: logical.MFARequirement.mfa_constraints:type_name -> logical.MFARequirement.MFAConstraintsEntry + 4, // 7: logical.MFARequirement.MFAConstraintsEntry.value:type_name -> logical.MFAConstraintAny + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_sdk_logical_identity_proto_init() } @@ -455,6 +651,42 @@ func file_sdk_logical_identity_proto_init() { return nil } } + file_sdk_logical_identity_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MFAMethodID); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sdk_logical_identity_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MFAConstraintAny); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_sdk_logical_identity_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MFARequirement); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -462,7 +694,7 @@ func file_sdk_logical_identity_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_sdk_logical_identity_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 11, NumExtensions: 0, NumServices: 0, }, diff --git a/sdk/logical/identity.proto b/sdk/logical/identity.proto index 11c76782319a..ea2e373b18c6 100644 --- a/sdk/logical/identity.proto +++ b/sdk/logical/identity.proto @@ -73,4 +73,19 @@ message Group { // NamespaceID is the identifier of the namespace to which this group // belongs to. string namespace_id = 4; -} +} + +message MFAMethodID { + string type = 1; + string id = 2; + bool uses_passcode = 3; +} + +message MFAConstraintAny { + repeated MFAMethodID any = 1; +} + +message MFARequirement { + string mfa_request_id = 1; + map mfa_constraints = 2; +} diff --git a/sdk/logical/plugin.pb.go b/sdk/logical/plugin.pb.go index 46de77666df8..d4722ce09761 100644 --- a/sdk/logical/plugin.pb.go +++ b/sdk/logical/plugin.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: sdk/logical/plugin.proto package logical diff --git a/sdk/logical/translate_response.go b/sdk/logical/translate_response.go index 7ecb0d1305c9..de5ea8fdbe21 100644 --- a/sdk/logical/translate_response.go +++ b/sdk/logical/translate_response.go @@ -39,6 +39,7 @@ func LogicalResponseToHTTPResponse(input *Response) *HTTPResponse { EntityID: input.Auth.EntityID, TokenType: input.Auth.TokenType.String(), Orphan: input.Auth.Orphan, + MFARequirement: input.Auth.MFARequirement, NumUses: input.Auth.NumUses, } } @@ -109,6 +110,7 @@ type HTTPAuth struct { EntityID string `json:"entity_id"` TokenType string `json:"token_type"` Orphan bool `json:"orphan"` + MFARequirement *MFARequirement `json:"mfa_requirement"` NumUses int `json:"num_uses"` } diff --git a/sdk/plugin/pb/backend.pb.go b/sdk/plugin/pb/backend.pb.go index 342670676c19..184717a97540 100644 --- a/sdk/plugin/pb/backend.pb.go +++ b/sdk/plugin/pb/backend.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: sdk/plugin/pb/backend.proto package pb diff --git a/vault/activity/activity_log.pb.go b/vault/activity/activity_log.pb.go index d59d3d3e17a8..abd0d5fb0067 100644 --- a/vault/activity/activity_log.pb.go +++ b/vault/activity/activity_log.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: vault/activity/activity_log.proto package activity diff --git a/vault/core.go b/vault/core.go index 00521ff5e273..a3ec323cd642 100644 --- a/vault/core.go +++ b/vault/core.go @@ -74,6 +74,10 @@ const ( coreKeyringCanaryPath = "core/canary-keyring" indexHeaderHMACKeyPath = "core/index-header-hmac-key" + + // defaultMFAAuthResponseTTL is the default duration that Vault caches the + // MfaAuthResponse when the value is not specified in the server config + defaultMFAAuthResponseTTL = 300 * time.Second ) var ( @@ -331,7 +335,8 @@ type Core struct { auditedHeaders *AuditedHeadersConfig // systemBackend is the backend which is used to manage internal operations - systemBackend *SystemBackend + systemBackend *SystemBackend + loginMFABackend *LoginMFABackend // cubbyholeBackend is the backend which manages the per-token storage cubbyholeBackend *CubbyholeBackend @@ -368,6 +373,10 @@ type Core struct { // inFlightReqMap is used to store info about in-flight requests inFlightReqData *InFlightRequests + // mfaResponseAuthQueue is used to cache the auth response per request ID + mfaResponseAuthQueue *LoginMFAPriorityQueue + mfaResponseAuthQueueLock sync.Mutex + // metricSink is the destination for all metrics that have // a cluster label. metricSink *metricsutil.ClusterMetricSink @@ -977,6 +986,8 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.ha = conf.HAPhysical } + c.loginMFABackend = NewLoginMFABackend(c, conf.Logger) + logicalBackends := make(map[string]logical.Factory) for k, f := range conf.LogicalBackends { logicalBackends[k] = f @@ -2064,6 +2075,7 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, c if err := c.setupQuotas(ctx, false); err != nil { return err } + c.setupCachedMFAResponseAuth() if !c.IsDRSecondary() { if err := c.startRollback(); err != nil { return err @@ -2234,6 +2246,11 @@ func (c *Core) preSeal() error { result = multierror.Append(result, fmt.Errorf("error stopping expiration: %w", err)) } c.stopActivityLog() + // Clear any cached auth response + c.mfaResponseAuthQueueLock.Lock() + c.mfaResponseAuthQueue = nil + c.mfaResponseAuthQueueLock.Unlock() + if err := c.teardownCredentials(context.Background()); err != nil { result = multierror.Append(result, fmt.Errorf("error tearing down credentials: %w", err)) } @@ -2973,6 +2990,57 @@ type LicenseState struct { Terminated bool } +type MFACachedAuthResponse struct { + CachedAuth *logical.Auth + RequestPath string + RequestNSID string + RequestNSPath string + RequestConnRemoteAddr string + TimeOfStorage time.Time + RequestID string +} + +func (c *Core) setupCachedMFAResponseAuth() { + c.mfaResponseAuthQueueLock.Lock() + c.mfaResponseAuthQueue = NewLoginMFAPriorityQueue() + mfaQueue := c.mfaResponseAuthQueue + c.mfaResponseAuthQueueLock.Unlock() + + ctx := c.activeContext + + go func() { + ticker := time.Tick(5 * time.Second) + for { + select { + case <-ctx.Done(): + return + case <-ticker: + err := mfaQueue.RemoveExpiredMfaAuthResponse(defaultMFAAuthResponseTTL, time.Now()) + if err != nil { + c.Logger().Error("failed to remove stale MFA auth response", "error", err) + } + } + } + }() + return +} + +// PopMFAResponseAuthByID pops an item from the mfaResponseAuthQueue by ID +// it returns the cached auth response or an error +func (c *Core) PopMFAResponseAuthByID(reqID string) (*MFACachedAuthResponse, error) { + c.mfaResponseAuthQueueLock.Lock() + defer c.mfaResponseAuthQueueLock.Unlock() + return c.mfaResponseAuthQueue.PopByKey(reqID) +} + +// SaveMFAResponseAuth pushes an MFACachedAuthResponse to the mfaResponseAuthQueue. +// it returns an error in case of failure +func (c *Core) SaveMFAResponseAuth(respAuth *MFACachedAuthResponse) error { + c.mfaResponseAuthQueueLock.Lock() + defer c.mfaResponseAuthQueueLock.Unlock() + return c.mfaResponseAuthQueue.Push(respAuth) +} + type InFlightRequests struct { InFlightReqMap *sync.Map InFlightReqCount *uberAtomic.Uint64 diff --git a/vault/core_util.go b/vault/core_util.go index e30cf46ba5bb..5200c461d8fe 100644 --- a/vault/core_util.go +++ b/vault/core_util.go @@ -123,10 +123,6 @@ func (c *Core) collectNamespaces() []*namespace.Namespace { } } -func (c *Core) namepaceByPath(string) *namespace.Namespace { - return namespace.RootNamespace -} - func (c *Core) setupReplicatedClusterPrimary(*replication.Cluster) error { return nil } func (c *Core) perfStandbyCount() int { return 0 } diff --git a/vault/external_tests/identity/login_mfa_duo_test.go b/vault/external_tests/identity/login_mfa_duo_test.go new file mode 100644 index 000000000000..1dc90d407765 --- /dev/null +++ b/vault/external_tests/identity/login_mfa_duo_test.go @@ -0,0 +1,290 @@ +package identity + +import ( + "fmt" + "net/http" + "reflect" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/userpass" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +var identityMFACoreConfigDUO = &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, +} + +var ( + secret_key = "" + integration_key = "" + api_hostname = "" +) + +func TestInteg_PolicyMFADUO(t *testing.T) { + t.Skip("This test requires manual intervention and DUO verify on cellphone is needed") + cluster := vault.NewTestCluster(t, identityMFACoreConfigDUO, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + // Enable Userpass authentication + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + err = mfaGeneratePolicyDUOTest(client) + if err != nil { + t.Fatalf("DUO verification failed") + } +} + +func mfaGeneratePolicyDUOTest(client *api.Client) error { + var err error + + rules := ` +path "secret/foo" { + capabilities = ["read"] + mfa_methods = ["my_duo"] +} + ` + + auths, err := client.Sys().ListAuth() + if err != nil { + return fmt.Errorf("failed to list auth mount") + } + mountAccessor := auths["userpass/"].Accessor + + err = client.Sys().PutPolicy("mfa_policy", rules) + if err != nil { + return fmt.Errorf("failed to create mfa_policy: %v", err) + } + + _, err = client.Logical().Write("auth/userpass/users/vaultmfa", map[string]interface{}{ + "password": "testpassword", + "policies": "mfa_policy", + }) + if err != nil { + return fmt.Errorf("failed to configure userpass backend: %v", err) + } + + secret, err := client.Logical().Write("auth/userpass/login/vaultmfa", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to login using userpass auth: %v", err) + } + + userpassToken := secret.Auth.ClientToken + + secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{ + "token": userpassToken, + }) + if err != nil { + return fmt.Errorf("failed to lookup userpass authenticated token: %v", err) + } + + // entityID := secret.Data["entity_id"].(string) + + mfaConfigData := map[string]interface{}{ + "mount_accessor": mountAccessor, + "secret_key": secret_key, + "integration_key": integration_key, + "api_hostname": api_hostname, + } + _, err = client.Logical().Write("sys/mfa/method/duo/my_duo", mfaConfigData) + if err != nil { + return fmt.Errorf("failed to persist TOTP MFA configuration: %v", err) + } + + // Write some data in the path that requires TOTP MFA + genericData := map[string]interface{}{ + "somedata": "which can only be read if MFA succeeds", + } + _, err = client.Logical().Write("secret/foo", genericData) + if err != nil { + return fmt.Errorf("failed to store data in generic backend: %v", err) + } + + // Replace the token in client with the one that has access to MFA + // required path + originalToken := client.Token() + defer client.SetToken(originalToken) + client.SetToken(userpassToken) + + // Create a GET request and set the MFA header containing the generated + // TOTP passcode + secretRequest := client.NewRequest("GET", "/v1/secret/foo") + secretRequest.Headers = make(http.Header) + // mfaHeaderValue := "my_duo:" + totpPasscode + // secretRequest.Headers.Add("X-Vault-MFA", mfaHeaderValue) + + // Make the request + resp, err := client.RawRequest(secretRequest) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 403 { + return fmt.Errorf("failed to read the secret") + } + if err != nil { + return fmt.Errorf("failed to read the secret: %v", err) + } + + // It should be possible to access the secret + secret, err = api.ParseSecret(resp.Body) + if err != nil { + return fmt.Errorf("failed to parse the secret: %v", err) + } + if !reflect.DeepEqual(secret.Data, genericData) { + return fmt.Errorf("bad: generic data; expected: %#v\nactual: %#v", genericData, secret.Data) + } + return nil +} + +func TestInteg_LoginMFADUO(t *testing.T) { + t.Skip("This test requires manual intervention and DUO verify on cellphone is needed") + cluster := vault.NewTestCluster(t, identityMFACoreConfigDUO, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + // Enable Userpass authentication + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + err = mfaGenerateLoginDUOTest(client) + if err != nil { + t.Fatalf("DUO verification failed. error: %s", err) + } +} + +func mfaGenerateLoginDUOTest(client *api.Client) error { + var err error + + auths, err := client.Sys().ListAuth() + if err != nil { + return fmt.Errorf("failed to list auth mount") + } + mountAccessor := auths["userpass/"].Accessor + + _, err = client.Logical().Write("auth/userpass/users/vaultmfa", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to configure userpass backend: %v", err) + } + secret, err := client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "test-entity", + }) + if err != nil { + return fmt.Errorf("failed to create an entity") + } + entityID := secret.Data["id"].(string) + + _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "vaultmfa", + "canonical_id": entityID, + "mount_accessor": mountAccessor, + }) + if err != nil { + return fmt.Errorf("failed to create an entity alias") + } + + var methodID string + // login MFA + { + // create a config + mfaConfigData := map[string]interface{}{ + "mount_accessor": mountAccessor, + "secret_key": secret_key, + "integration_key": integration_key, + "api_hostname": api_hostname, + } + resp, err := client.Logical().Write("identity/mfa/method-id/duo", mfaConfigData) + + if err != nil || (resp == nil) { + return fmt.Errorf("bad: resp: %#v\n err: %v", resp, err) + } + + methodID = resp.Data["method_id"].(string) + if methodID == "" { + return fmt.Errorf("method ID is empty") + } + + // 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_entity_ids": []string{entityID}, + "name": "randomName", + "mfa_method_ids": []string{methodID}, + }) + if err != nil { + return fmt.Errorf("failed to configure MFAEnforcementConfig: %v", err) + } + } + + secret, err = client.Logical().Write("auth/userpass/login/vaultmfa", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to login using userpass auth: %v", err) + } + + if secret.Auth == nil || secret.Auth.MFARequirement == nil { + return fmt.Errorf("two phase login returned nil MFARequirement") + } + if secret.Auth.MFARequirement.MFARequestID == "" { + return fmt.Errorf("MFARequirement contains empty MFARequestID") + } + if secret.Auth.MFARequirement.MFAConstraints == nil || len(secret.Auth.MFARequirement.MFAConstraints) == 0 { + return fmt.Errorf("MFAConstraints is nil or empty") + } + mfaConstraints, ok := secret.Auth.MFARequirement.MFAConstraints["randomName"] + if !ok { + return fmt.Errorf("failed to find the mfaConstrains") + } + if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 { + return fmt.Errorf("") + } + for _, mfaAny := range mfaConstraints.Any { + if mfaAny.ID != methodID || mfaAny.Type != "duo" { + return fmt.Errorf("invalid mfa constraints") + } + } + + // validation + secret, err = client.Logical().Write("sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {}, + }, + }) + if err != nil { + return fmt.Errorf("MFA failed: %v", err) + } + + if secret.Auth.ClientToken == "" { + return fmt.Errorf("MFA was not enforced") + } + + return nil +} diff --git a/vault/external_tests/identity/login_mfa_okta_test.go b/vault/external_tests/identity/login_mfa_okta_test.go new file mode 100644 index 000000000000..c80825af4a33 --- /dev/null +++ b/vault/external_tests/identity/login_mfa_okta_test.go @@ -0,0 +1,354 @@ +package identity + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/okta" + "github.com/hashicorp/vault/builtin/credential/userpass" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +var ( + org_name = "" + api_token = "" +) + +var identityOktaMFACoreConfig = &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + "okta": okta.Factory, + }, +} + +func TestOktaEngineMFA(t *testing.T) { + t.Skip("This test requires manual intervention and OKTA verify on cellphone is needed") + cluster := vault.NewTestCluster(t, identityOktaMFACoreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + // Enable Okta engine + err := client.Sys().EnableAuthWithOptions("okta", &api.EnableAuthOptions{ + Type: "okta", + }) + if err != nil { + t.Fatalf("failed to enable okta auth: %v", err) + } + + _, err = client.Logical().Write("auth/okta/config", map[string]interface{}{ + "base_url": "okta.com", + "org_name": org_name, + "api_token": api_token, + }) + if err != nil { + t.Fatalf("error configuring okta mount: %v", err) + } + + _, err = client.Logical().Write("auth/okta/groups/testgroup", map[string]interface{}{ + "policies": "default", + }) + if err != nil { + t.Fatalf("error configuring okta group, %v", err) + } + + _, err = client.Logical().Write("auth/okta/login/", map[string]interface{}{ + "password": "", + }) + if err != nil { + t.Fatalf("error configuring okta group, %v", err) + } +} + +func TestInteg_PolicyMFAOkta(t *testing.T) { + t.Skip("This test requires manual intervention and OKTA verify on cellphone is needed") + cluster := vault.NewTestCluster(t, identityOktaMFACoreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + // Enable Userpass authentication + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + err = mfaGenerateOktaPolicyMFATest(client) + if err != nil { + t.Fatalf("Okta failed: %s", err) + } +} + +func mfaGenerateOktaPolicyMFATest(client *api.Client) error { + var err error + + rules := ` +path "secret/foo" { + capabilities = ["read"] + mfa_methods = ["my_okta"] +} + ` + + err = client.Sys().PutPolicy("mfa_policy", rules) + if err != nil { + return fmt.Errorf("failed to create mfa_policy: %v", err) + } + + // listing auth mounts to find the mount accessor for the userpass + auths, err := client.Sys().ListAuth() + if err != nil { + return fmt.Errorf("error listing auth mounts") + } + mountAccessor := auths["userpass/"].Accessor + + // creating a user in userpass + _, err = client.Logical().Write("auth/userpass/users/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to configure userpass backend: %v", err) + } + + // creating an identity with email metadata to be used for MFA validation + secret, err := client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "test-entity", + "policies": "mfa_policy", + "metadata": map[string]string{ + "email": "", + }, + }) + if err != nil { + return fmt.Errorf("failed to create an entity") + } + entityID := secret.Data["id"].(string) + + // assigning the entity ID to the testuser alias + _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "testuser", + "canonical_id": entityID, + "mount_accessor": mountAccessor, + }) + if err != nil { + return fmt.Errorf("failed to create an entity alias") + } + + mfaConfigData := map[string]interface{}{ + "mount_accessor": mountAccessor, + "org_name": org_name, + "api_token": api_token, + "primary_email": true, + "username_format": "{{entity.metadata.email}}", + } + _, err = client.Logical().Write("sys/mfa/method/okta/my_okta", mfaConfigData) + if err != nil { + return fmt.Errorf("failed to persist TOTP MFA configuration: %v", err) + } + + // Write some data in the path that requires TOTP MFA + genericData := map[string]interface{}{ + "somedata": "which can only be read if MFA succeeds", + } + _, err = client.Logical().Write("secret/foo", genericData) + if err != nil { + return fmt.Errorf("failed to store data in generic backend: %v", err) + } + + // Replace the token in client with the one that has access to MFA + // required path + originalToken := client.Token() + defer client.SetToken(originalToken) + + // login to the testuser + secret, err = client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to login using userpass auth: %v", err) + } + + userpassToken := secret.Auth.ClientToken + client.SetToken(userpassToken) + + secret, err = client.Logical().Read("secret/foo") + if err != nil { + return fmt.Errorf("failed to read the secret: %v", err) + } + + // It should be possible to access the secret + // secret, err = api.ParseSecret(resp.Body) + if err != nil { + return fmt.Errorf("failed to parse the secret: %v", err) + } + if !reflect.DeepEqual(secret.Data, genericData) { + return fmt.Errorf("bad: generic data; expected: %#v\nactual: %#v", genericData, secret.Data) + } + return nil +} + +func TestInteg_LoginMFAOkta(t *testing.T) { + t.Skip("This test requires manual intervention and OKTA verify on cellphone is needed") + cluster := vault.NewTestCluster(t, identityOktaMFACoreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + // Enable Userpass authentication + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + err = mfaGenerateOktaLoginMFATest(client) + if err != nil { + t.Fatalf("Okta failed: %s", err) + } +} + +func mfaGenerateOktaLoginMFATest(client *api.Client) error { + var err error + + auths, err := client.Sys().ListAuth() + if err != nil { + return fmt.Errorf("failed to list auth mounts") + } + mountAccessor := auths["userpass/"].Accessor + + _, err = client.Logical().Write("auth/userpass/users/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to configure userpass backend: %v", err) + } + + secret, err := client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "test-entity", + "metadata": map[string]string{ + "email": "", + }, + }) + if err != nil { + return fmt.Errorf("failed to create an entity") + } + entityID := secret.Data["id"].(string) + + _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "testuser", + "canonical_id": entityID, + "mount_accessor": mountAccessor, + }) + if err != nil { + return fmt.Errorf("failed to create an entity alias") + } + + var methodID string + var userpassToken string + // login MFA + { + // create a config + mfaConfigData := map[string]interface{}{ + "mount_accessor": mountAccessor, + "org_name": org_name, + "api_token": api_token, + "primary_email": true, + "username_format": "{{entity.metadata.email}}", + } + resp, err := client.Logical().Write("identity/mfa/method-id/okta", mfaConfigData) + + if err != nil || (resp == nil) { + return fmt.Errorf("bad: resp: %#v\n err: %v", resp, err) + } + + methodID = resp.Data["method_id"].(string) + if methodID == "" { + return fmt.Errorf("method ID is empty") + } + // 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_entity_ids": []string{entityID}, + "name": "randomName", + "mfa_method_ids": []string{methodID}, + }) + if err != nil { + return fmt.Errorf("failed to configure MFAEnforcementConfig: %v", err) + } + } + + secret, err = client.Logical().Write("auth/userpass/login/testuser", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + return fmt.Errorf("failed to login using userpass auth: %v", err) + } + + if secret.Auth == nil || secret.Auth.MFARequirement == nil { + return fmt.Errorf("two phase login returned nil MFARequirement") + } + if secret.Auth.MFARequirement.MFARequestID == "" { + return fmt.Errorf("MFARequirement contains empty MFARequestID") + } + if secret.Auth.MFARequirement.MFAConstraints == nil || len(secret.Auth.MFARequirement.MFAConstraints) == 0 { + return fmt.Errorf("MFAConstraints is nil or empty") + } + mfaConstraints, ok := secret.Auth.MFARequirement.MFAConstraints["randomName"] + if !ok { + return fmt.Errorf("failed to find the mfaConstrains") + } + if mfaConstraints.Any == nil || len(mfaConstraints.Any) == 0 { + return fmt.Errorf("") + } + for _, mfaAny := range mfaConstraints.Any { + if mfaAny.ID != methodID || mfaAny.Type != "okta" { + return fmt.Errorf("invalid mfa constraints") + } + } + + // validation + secret, err = client.Logical().Write("sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {}, + }, + }) + if err != nil { + return fmt.Errorf("MFA failed: %v", err) + } + + userpassToken = secret.Auth.ClientToken + if secret.Auth.ClientToken == "" { + return fmt.Errorf("MFA was not enforced") + } + + client.SetToken(client.Token()) + secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{ + "token": userpassToken, + }) + if err != nil { + return fmt.Errorf("failed to lookup userpass authenticated token: %v", err) + } + + entityIDCheck := secret.Data["entity_id"].(string) + if entityIDCheck != entityID { + return fmt.Errorf("different entityID assigned") + } + + return nil +} diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go new file mode 100644 index 000000000000..6d42fb6dd6e9 --- /dev/null +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -0,0 +1,886 @@ +package identity + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/builtin/logical/totp" + "github.com/hashicorp/vault/helper/testhelpers" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +var loginMFACoreConfig = &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + LogicalBackends: map[string]logical.Factory{ + "totp": totp.Factory, + }, +} + +type totpCode struct { + name string + methodID string + namespacePath string // this is tied to the entityID or the mount accessor + entityID string +} + +func getNamespaceSpecificMountAccessor(namespace string, client *api.Client, t *testing.T) string { + client.SetNamespace(namespace) + auths, err := client.Sys().ListAuth() + if err != nil || auths == nil || auths["userpass/"] == nil { + t.Fatalf("failed to get the list of auths") + } + return auths["userpass/"].Accessor +} + +func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { + cluster := vault.NewTestCluster(t, loginMFACoreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + client := cluster.Cores[0].Client + + // Mount the TOTP backend + mountInfo := &api.MountInput{ + Type: "totp", + } + err := client.Sys().Mount("totp", mountInfo) + if err != nil { + t.Fatalf("failed to mount totp backend: %v", err) + } + + // Enable Userpass authentication + err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + 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() + if err != nil { + t.Fatalf("bb") + } + var mountAccessor string + if auths != nil && auths["userpass/"] != nil { + 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") + } + + } + + // 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-id/totp", map[string]interface{}{ + "issuer": "yCorp", + "period": 10000, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": 10, + "qr_size": 100, + }) + + if err != nil || (resp1 == nil) { + t.Fatalf("bad: resp: %#v\n err: %v", resp1, err) + } + + methodID = resp1.Data["method_id"].(string) + if methodID == "" { + t.Fatalf("method ID is empty") + } + + secret, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/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}, + }) + 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) + } + + userpassToken = secret.Auth.ClientToken + + 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) + } + + entityIDCheck := secret.Data["entity_id"].(string) + if entityIDCheck != entityID { + t.Fatalf("different entityID assigned") + } + + // 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) + } + + 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") + } + } + + // validation + 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) + } + + // 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") + } + // give it enough time to make sure the request has expired + time.Sleep(605 * time.Second) + _, 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: %v", err) + } + + // Destroy the secret so that the token can self generate + _, err = userClient.Logical().Write(fmt.Sprintf("identity/mfa/method-id/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) + } + } +} + +//- an enforcement can be defined in any NS and applies to that NS and its children +//- a methodid can be defined in any NS and may be referenced by an enforcement in +// that NS or its children +//- an entity may configure TOTP keys for methods defined in the entity's NS or +// its parents +func TestNamespaceLoginMfaTotp(t *testing.T) { + cluster := vault.NewTestCluster(t, loginMFACoreConfig, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + + client := cluster.Cores[0].Client + + // Mount the TOTP backend + mountInfo := &api.MountInput{ + Type: "totp", + } + err := client.Sys().Mount("totp", mountInfo) + if err != nil { + t.Fatalf("failed to mount totp backend: %v", err) + } + + // Mount Userpass for the root namespace + err = client.Sys().EnableAuthWithOptions("userpassRoot", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + // Setup Namespaces and create Userpass mounts in each + // creating namespaces ns1, ns1/ns2, and ns1/ns2/ns3 + + // NS1 + ns1Path := testhelpers.CreateNamespace(t, client, "ns1/", "") + + // Enable Userpass authentication for the NS + client.SetNamespace(ns1Path) + err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatalf("bb") + } + mountAccessor1 := auths["userpass/"].Accessor + + resp, err := client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "test-entity1", + "metadata": map[string]string{ + "email": "test@hashicorp.com", + "phone_number": "123-456-7890", + }, + }) + if err != nil { + t.Fatalf("failed to creat an entity: %v", err) + } + entityID1 := resp.Data["id"].(string) + + _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "testuser1", + "canonical_id": entityID1, + "mount_accessor": getNamespaceSpecificMountAccessor(ns1Path, client, t), + }) + if err != nil { + t.Fatalf("failed to create an entity-alias: %v", err) + } + + // Creating a user in the userpass auth mount + _, err = client.Logical().Write("auth/userpass/users/testuser1", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("failed to configure userpass backend: %v", err) + } + + // NS2 + // Create second namespace ns2 + ns2Path := testhelpers.CreateNamespace(t, client, "ns2/", "ns1/") + client.SetNamespace(ns2Path) + err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + auths, err = client.Sys().ListAuth() + if err != nil { + t.Fatalf("bb") + } + mountAccessor2 := auths["userpass/"].Accessor + + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "test-entity2", + "metadata": map[string]string{ + "email": "test@hashicorp.com", + "phone_number": "123-456-7890", + }, + }) + if err != nil { + t.Fatalf("failed to creat an entity: %v", err) + } + entityID2 := resp.Data["id"].(string) + + _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "testuser2", + "canonical_id": entityID2, + "mount_accessor": getNamespaceSpecificMountAccessor(ns2Path, client, t), + }) + if err != nil { + t.Fatalf("failed to create an entity-alias: %v", err) + } + // Creating a user in the userpass auth mount + _, err = client.Logical().Write("auth/userpass/users/testuser2", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("failed to configure userpass backend: %v", err) + } + + // NS3 + // Create third namespace within ns1 + ns3Path := testhelpers.CreateNamespace(t, client, "ns3/", "ns1/ns2/") + client.SetNamespace(ns3Path) + err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + auths, err = client.Sys().ListAuth() + if err != nil { + t.Fatalf("bb") + } + mountAccessor3 := auths["userpass/"].Accessor + // mountAccessors := []string{mountAccessor1, mountAccessor2, mountAccessor3} + + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{ + "name": "test-entity3", + "metadata": map[string]string{ + "email": "test@hashicorp.com", + "phone_number": "123-456-7890", + }, + }) + if err != nil { + t.Fatalf("failed to creat an entity: %v", err) + } + entityID3 := resp.Data["id"].(string) + + _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ + "name": "testuser3", + "canonical_id": entityID3, + "mount_accessor": getNamespaceSpecificMountAccessor(ns3Path, client, t), + }) + if err != nil { + t.Fatalf("failed to create an entity-alias: %v", err) + } + // Creating a user in the userpass auth mount + _, err = client.Logical().Write("auth/userpass/users/testuser3", map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("failed to configure userpass backend: %v", err) + } + + namespaceEntityIDs := []string{entityID1, entityID2, entityID3} + + // Creating a group for all entities in all namespaces + client.SetNamespace("") + // Create a group + resp, err = client.Logical().Write("identity/group", map[string]interface{}{ + "name": "engineering", + "member_entity_ids": []string{entityID1, entityID2, entityID3}, + }) + if err != nil { + t.Fatalf("failed to create a group: %v", err) + } + + // groupID := resp.Data["id"].(string) + + namespacePaths := []string{ns1Path, ns2Path, ns3Path} + var namespaceMethodIDs []string + for _, nsPath := range namespacePaths { + // MFA TOTP Method for various NS + client.SetNamespace(nsPath) + // create a config + resp, err = client.Logical().Write("identity/mfa/method-id/totp", map[string]interface{}{ + "issuer": "yCorp", + "period": 10000, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": 10, + "qr_size": 100, + }) + + if err != nil || (resp == nil) { + t.Fatalf("bad: resp: %#v\n err: %v", resp, err) + } + + methodID := resp.Data["method_id"].(string) + if methodID == "" { + t.Fatalf("method ID is empty") + } + namespaceMethodIDs = append(namespaceMethodIDs, methodID) + } + + methodIDTotpCodeNameMap := checkGenerateTotp(client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs, t) + + // Creating Login enforcement in NS1 + client.SetNamespace(ns1Path) + // creating MFAEnforcementConfig + _, err = client.Logical().Write("identity/mfa/login-enforcement/LE11", map[string]interface{}{ + "auth_method_accessors": []string{mountAccessor1}, + "identity_entity_ids": []string{entityID1}, + "name": "LE11", + "mfa_method_ids": []string{namespaceMethodIDs[0]}, + }) + if err != nil { + t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) + } + + // creating MFAEnforcementConfig + client.SetNamespace(ns2Path) + _, err = client.Logical().Write("identity/mfa/login-enforcement/LE21", map[string]interface{}{ + "auth_method_accessors": []string{mountAccessor2}, + "identity_entity_ids": []string{entityID2}, + "name": "LE21", + "mfa_method_ids": []string{namespaceMethodIDs[0]}, + }) + if err != nil { + t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) + } + + _, err = client.Logical().Write("identity/mfa/login-enforcement/LE22", map[string]interface{}{ + "auth_method_accessors": []string{mountAccessor2}, + "identity_entity_ids": []string{entityID2}, + "name": "LE22", + "mfa_method_ids": []string{namespaceMethodIDs[1]}, + }) + if err != nil { + t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) + } + + client.SetNamespace(ns3Path) + // creating MFAEnforcementConfig + _, err = client.Logical().Write("identity/mfa/login-enforcement/LE33", map[string]interface{}{ + "auth_method_accessors": []string{mountAccessor3}, + "identity_entity_ids": []string{entityID3}, + "name": "LE33", + "mfa_method_ids": []string{namespaceMethodIDs[2]}, + }) + if err != nil { + t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) + } + + singlePhaseLogin(client, t, "testuser1", entityID1, methodIDTotpCodeNameMap) + singlePhaseLogin(client, t, "testuser2", entityID2, methodIDTotpCodeNameMap) + singlePhaseLogin(client, t, "testuser3", entityID3, methodIDTotpCodeNameMap) + twoPhaseLogin(client, t, "testuser1", entityID1, methodIDTotpCodeNameMap) + twoPhaseLogin(client, t, "testuser2", entityID2, methodIDTotpCodeNameMap) + twoPhaseLogin(client, t, "testuser3", entityID3, methodIDTotpCodeNameMap) + twoPhaseLoginDifferentNamespace(client, t, "testuser3", entityID3, methodIDTotpCodeNameMap) + + checkDestroyTotp(client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs, t) +} + +func singlePhaseLogin(client *api.Client, t *testing.T, username, entityID string, totpCodeMap map[string][]*totpCode) { + headers := client.Headers() + headers.Del("X-Vault-MFA") + client.SetHeaders(headers) + + // getting the passcode + client.SetNamespace("") + + totpCodeStruct := totpCodeMap[entityID] + for _, codeStruct := range totpCodeStruct { + secret, err := client.Logical().Read(fmt.Sprintf("totp/code/%s", codeStruct.name)) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + passCode, ok := secret.Data["code"].(string) + if !ok && passCode == "" { + t.Fatalf("failed to generate a totp passcode") + } + + // MFA single-phase login + client.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", codeStruct.methodID, passCode)) + } + + // namespace is the same for the same entityID + client.SetNamespace(totpCodeStruct[0].namespacePath) + secret, err := client.Logical().Write(fmt.Sprintf("auth/userpass/login/%s", username), map[string]interface{}{ + "password": "testpassword", + }) + if err != nil { + t.Fatalf("%s MFA failed: %v", username, err) + } + + userpassToken := secret.Auth.ClientToken + + secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{ + "token": userpassToken, + }) + if err != nil { + t.Fatalf("failed to lookup userpass authenticated token: %v", err) + } + + entityIDCheck := secret.Data["entity_id"].(string) + if entityIDCheck != entityID { + t.Fatalf("different entityID assigned") + } +} + +func twoPhaseLogin(client *api.Client, t *testing.T, username, entityID string, totpCodeMap map[string][]*totpCode) { + // Two-phase login + headers := client.Headers() + headers.Del("X-Vault-MFA") + client.SetHeaders(headers) + + client.SetNamespace("") + totpCodeStruct := totpCodeMap[entityID] + + methodIDPasscodeMap := make(map[string][]string, 0) + for _, codeStruct := range totpCodeStruct { + secret, err := client.Logical().Read(fmt.Sprintf("totp/code/%s", codeStruct.name)) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + passCode, ok := secret.Data["code"].(string) + if !ok && passCode == "" { + t.Fatalf("failed to generate a totp passcode") + } + methodIDPasscodeMap[codeStruct.methodID] = []string{passCode} + } + + // namespace is the same for the same entityID + client.SetNamespace(totpCodeStruct[0].namespacePath) + + secret, err := client.Logical().Write(fmt.Sprintf("auth/userpass/login/%s", username), 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 for username %s", username) + } + 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") + } + + // validation + secretValidated, err := client.Logical().Write("sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": methodIDPasscodeMap, + }) + if err != nil || secretValidated == nil { + t.Fatalf("MFA failed: %v", err) + } + + // validate the same request the second time should fail + secret, err = client.Logical().Write("sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": methodIDPasscodeMap, + }) + if err == nil { + t.Fatalf("MFA validate did not fail as expected") + } + if !strings.Contains(err.Error(), "invalid request ID") { + t.Fatalf("expected error invalid request ID, got %s", err.Error()) + } +} + +func twoPhaseLoginDifferentNamespace(client *api.Client, t *testing.T, username, entityID string, totpCodeMap map[string][]*totpCode) { + // Two-phase login + headers := client.Headers() + headers.Del("X-Vault-MFA") + client.SetHeaders(headers) + + client.SetNamespace("") + totpCodeStruct := totpCodeMap[entityID] + + methodIDPasscodeMap := make(map[string][]string, 0) + for _, codeStruct := range totpCodeStruct { + secret, err := client.Logical().Read(fmt.Sprintf("totp/code/%s", codeStruct.name)) + if err != nil { + t.Fatalf("failed to create totp passcode: %v", err) + } + passCode, ok := secret.Data["code"].(string) + if !ok && passCode == "" { + t.Fatalf("failed to generate a totp passcode") + } + methodIDPasscodeMap[codeStruct.methodID] = []string{passCode} + } + + // namespace is the same for the same entityID + client.SetNamespace(totpCodeStruct[0].namespacePath) + + secret, err := client.Logical().Write(fmt.Sprintf("auth/userpass/login/%s", username), 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 for username %s", username) + } + 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") + } + + // validation in a different namespace + client.SetNamespace("") + _, err = client.Logical().Write("sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": methodIDPasscodeMap, + }) + if err == nil { + t.Fatalf("expected MFA validate to fail: %v", err) + } + if !strings.Contains(err.Error(), "original request was issued in a different namesapce") { + t.Fatalf("unexpected error returned") + } +} + +func checkGenerateTotp(client *api.Client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs []string, t *testing.T) map[string][]*totpCode { + codeNameStructMap := make(map[string][]*totpCode, 0) + + // generating totp in different namespace than entity namespace should fail + client.SetNamespace("") + secret, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[0], + "method_id": namespaceMethodIDs[1], + }) + if err == nil { + t.Fatalf("1failed to generate a TOTP secret on an entity: %v", err) + } + + // non-root namespace + client.SetNamespace(namespacePaths[2]) + secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[2], + "method_id": namespaceMethodIDs[2], + }) + if err != nil { + t.Fatalf("2failed to generate a TOTP secret on an entity: %v", err) + } + totpURL := secret.Data["url"].(string) + + name := namespaceMethodIDs[2] + namespaceEntityIDs[2] + registerTotpUrl(client, totpURL, name, t) + codeNameStructMap[namespaceEntityIDs[2]] = append(codeNameStructMap[namespaceEntityIDs[2]], &totpCode{ + name: name, + methodID: namespaceMethodIDs[2], + namespacePath: namespacePaths[2], + entityID: namespaceEntityIDs[2], + }) + + client.SetNamespace(namespacePaths[0]) + secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[0], + "method_id": namespaceMethodIDs[0], + }) + if err != nil { + t.Fatalf("3failed to generate a TOTP secret on an entity: %v", err) + } + totpURL = secret.Data["url"].(string) + + name = namespaceMethodIDs[0] + namespaceEntityIDs[0] + registerTotpUrl(client, totpURL, name, t) + codeNameStructMap[namespaceEntityIDs[0]] = append(codeNameStructMap[namespaceEntityIDs[0]], &totpCode{ + name: name, + methodID: namespaceMethodIDs[0], + namespacePath: namespacePaths[0], + entityID: namespaceEntityIDs[0], + }) + + client.SetNamespace(namespacePaths[1]) + secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[1], + "method_id": namespaceMethodIDs[0], + }) + if err != nil { + t.Fatalf("failed to generate a TOTP secret on an entity: %v", err) + } + totpURL = secret.Data["url"].(string) + + name = namespaceMethodIDs[0] + namespaceEntityIDs[1] + registerTotpUrl(client, totpURL, name, t) + codeNameStructMap[namespaceEntityIDs[1]] = append(codeNameStructMap[namespaceEntityIDs[1]], &totpCode{ + name: name, + methodID: namespaceMethodIDs[0], + namespacePath: namespacePaths[1], + entityID: namespaceEntityIDs[1], + }) + + client.SetNamespace(namespacePaths[1]) + secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[1], + "method_id": namespaceMethodIDs[1], + }) + if err != nil { + t.Fatalf("5failed to generate a TOTP secret on an entity: %v", err) + } + totpURL = secret.Data["url"].(string) + + name = namespaceMethodIDs[1] + namespaceEntityIDs[1] + registerTotpUrl(client, totpURL, name, t) + codeNameStructMap[namespaceEntityIDs[1]] = append(codeNameStructMap[namespaceEntityIDs[1]], &totpCode{ + name: name, + methodID: namespaceMethodIDs[1], + namespacePath: namespacePaths[1], + entityID: namespaceEntityIDs[1], + }) + + return codeNameStructMap +} + +func registerTotpUrl(client *api.Client, totpURL, codeName string, t *testing.T) { + client.SetNamespace("") + _, err := client.Logical().Write(fmt.Sprintf("totp/keys/%s", codeName), map[string]interface{}{ + "url": totpURL, + }) + if err != nil { + t.Fatalf("failed to register a TOTP URL: %v", err) + } +} + +func checkDestroyTotp(client *api.Client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs []string, t *testing.T) { + client.SetNamespace("") + + _, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[0], + "method_id": namespaceMethodIDs[1], + }) + if err == nil { + t.Fatalf("failed to destroy the MFA secret: %s", err) + } + + // non-root namespace + client.SetNamespace(namespacePaths[2]) + _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[2], + "method_id": namespaceMethodIDs[2], + }) + if err != nil { + t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) + } + + client.SetNamespace(namespacePaths[0]) + _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[0], + "method_id": namespaceMethodIDs[0], + }) + if err != nil { + t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) + } + + client.SetNamespace(namespacePaths[1]) + _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[1], + "method_id": namespaceMethodIDs[0], + }) + if err != nil { + t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) + } + + client.SetNamespace(namespacePaths[1]) + _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ + "entity_id": namespaceEntityIDs[1], + "method_id": namespaceMethodIDs[1], + }) + if err != nil { + t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) + } +} diff --git a/vault/external_tests/mfa/login_mfa_test.go b/vault/external_tests/mfa/login_mfa_test.go new file mode 100644 index 000000000000..8370a137c193 --- /dev/null +++ b/vault/external_tests/mfa/login_mfa_test.go @@ -0,0 +1,1382 @@ +package mfa + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/userpass" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +// TestLoginMFA_LoginEnforcement_UniqueNames tests to ensure that 2 different login enforcements can be created with +// the same name, as long as they're in separate namespaces. +func TestLoginMFA_LoginEnforcement_UniqueNames(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // create a few namespaces + _, err := client.Logical().Write("sys/namespaces/foo", nil) + if err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("sys/namespaces/bar", nil) + if err != nil { + t.Fatal(err) + } + + // create some prereq data + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "fooCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooConfigId := resp.Data["method_id"].(string) + + resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "barCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + barConfigId := resp.Data["method_id"].(string) + + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) + if err != nil { + t.Fatal(err) + } + bobId := resp.Data["id"].(string) + + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) + if err != nil { + t.Fatal(err) + } + aliceId := resp.Data["id"].(string) + + myPath := "identity/mfa/login-enforcement/baz" + + // create a login enforcement config in the foo ns + client.SetNamespace("foo") + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{fooConfigId}, + "identity_entity_ids": []string{bobId}, + }) + if err != nil { + t.Fatal(err) + } + + // create the same login enforcement config with the same name in the bar ns. + // this should succeed because enforcement config names are unique per ns, + // not globally. + client.SetNamespace("bar") + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{barConfigId}, + "identity_entity_ids": []string{aliceId}, + }) + if err != nil { + t.Fatal(err) + } + + // when we read the foo login enforcement config back out, it should have fooCorp and bob, not barCorp and alice + // because both baz login enforcements were stored separately, since they were in separate namespaces. if they + // weren't stored separately, the second write would've overwritten the first. + client.SetNamespace("foo") + resp, err = client.Logical().Read(myPath) + if err != nil { + t.Fatal(err) + } + if ieid := resp.Data["identity_entity_ids"].([]interface{})[0]; ieid != bobId { + t.Fatalf("expected bob but got %q", ieid) + } + if mmid := resp.Data["mfa_method_ids"].([]interface{})[0]; mmid != fooConfigId { + t.Fatalf("expected %q but got %q", fooConfigId, mmid) + } +} + +// TestLoginMFA_Method_CRUD tests creating/reading/updating/deleting a method config for all of the MFA providers +func TestLoginMFA_Method_CRUD(t *testing.T) { + cluster := vault.NewTestCluster(t, &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + }, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // Enable userpass authentication + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + mountAccessor := auths["userpass/"].Accessor + + testCases := []struct { + methodName string + configData map[string]interface{} + keyToUpdate string + valueToUpdate string + keyToCheck string + updatedValue string + }{ + { + "totp", + map[string]interface{}{ + "issuer": "yCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }, + "issuer", + "zCorp", + "", + "", + }, + { + "duo", + map[string]interface{}{ + "mount_accessor": mountAccessor, + "secret_key": "lol-secret", + "integration_key": "integration-key", + "api_hostname": "some-hostname", + }, + "api_hostname", + "api-updated.duosecurity.com", + "", + "", + }, + { + "okta", + map[string]interface{}{ + "mount_accessor": mountAccessor, + "base_url": "example.com", + "org_name": "my-org", + "api_token": "lol-token", + }, + "org_name", + "dev-62954466-updated", + "", + "", + }, + { + "pingid", + map[string]interface{}{ + "mount_accessor": mountAccessor, + "settings_file_base64": "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCm9yZ19hbGlhcz1sb2wtb3JnLWFsaWFzCmFkbWluX3VybD1odHRwczovL2lkcHhueWwzbS5waW5naWRlbnRpdHkuY29tL3BpbmdpZAphdXRoZW50aWNhdG9yX3VybD1odHRwczovL2F1dGhlbnRpY2F0b3IucGluZ29uZS5jb20vcGluZ2lkL3BwbQ==", + }, + "settings_file_base64", + "I0F1dG8tR2VuZXJhdGVkIGZyb20gUGluZ09uZSwgZG93bmxvYWRlZCBieSBpZD1bU1NPXSBlbWFpbD1baGFtaWRAaGFzaGljb3JwLmNvbV0KI1dlZCBEZWMgMTUgMTM6MDg6NDQgTVNUIDIwMjEKdXNlX2Jhc2U2NF9rZXk9YlhrdGMyVmpjbVYwTFd0bGVRPT0KdXNlX3NpZ25hdHVyZT10cnVlCnRva2VuPWxvbC10b2tlbgppZHBfdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkL3VwZGF0ZWQKb3JnX2FsaWFzPWxvbC1vcmctYWxpYXMKYWRtaW5fdXJsPWh0dHBzOi8vaWRweG55bDNtLnBpbmdpZGVudGl0eS5jb20vcGluZ2lkCmF1dGhlbnRpY2F0b3JfdXJsPWh0dHBzOi8vYXV0aGVudGljYXRvci5waW5nb25lLmNvbS9waW5naWQvcHBt", + "idp_url", + "https://idpxnyl3m.pingidentity.com/pingid/updated", + }, + } + + for _, tc := range testCases { + t.Run(tc.methodName, func(t *testing.T) { + // create a new method config + myPath := fmt.Sprintf("identity/mfa/method/%s", tc.methodName) + resp, err := client.Logical().Write(myPath, tc.configData) + if err != nil { + t.Fatal(err) + } + + methodId := resp.Data["method_id"] + if methodId == "" { + t.Fatal("method id is empty") + } + + myNewPath := fmt.Sprintf("%s/%s", myPath, methodId) + + // read it back + resp, err = client.Logical().Read(myNewPath) + if err != nil { + t.Fatal(err) + } + + if resp.Data["id"] != methodId { + t.Fatal("expected response id to match existing method id but it didn't") + } + + // listing should show it + resp, err = client.Logical().List(myPath) + if err != nil { + t.Fatal(err) + } + if resp.Data["keys"].([]interface{})[0] != methodId { + t.Fatalf("expected %q in the list of method ids but it wasn't there", methodId) + } + + // update it + tc.configData[tc.keyToUpdate] = tc.valueToUpdate + _, err = client.Logical().Write(myNewPath, tc.configData) + if err != nil { + t.Fatal(err) + } + + resp, err = client.Logical().Read(myNewPath) + if err != nil { + t.Fatal(err) + } + + // these shenanigans are to work around the arcane way that pingid does updates + if tc.keyToCheck != "" && tc.updatedValue != "" { + if resp.Data[tc.keyToCheck] != tc.updatedValue { + t.Fatalf("expected config to update but it didn't: %v != %v", resp.Data[tc.keyToCheck], tc.updatedValue) + } + } else { + if resp.Data[tc.keyToUpdate] != tc.valueToUpdate { + t.Fatalf("expected config to update but it didn't: %v != %v", resp.Data[tc.keyToUpdate], tc.valueToUpdate) + } + } + + // delete it + _, err = client.Logical().Delete(myNewPath) + if err != nil { + t.Fatal(err) + } + + // try to read it again - should 404 + resp, err = client.Logical().Read(myNewPath) + if !(resp == nil && err == nil) { + t.Fatal("expected a 404 but didn't get one") + } + }) + } +} + +// TestLoginMFA_LoginEnforcement_CRUD tests creating/reading/updating/deleting a login enforcement config +func TestLoginMFA_LoginEnforcement_CRUD(t *testing.T) { + cluster := vault.NewTestCluster(t, &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + }, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // first create a few configs + configIDs := make([]string, 0) + + for i := 0; i < 2; i++ { + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": fmt.Sprintf("fooCorp%d", i), + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100 + i, + }) + if err != nil { + t.Fatal(err) + } + + configIDs = append(configIDs, resp.Data["method_id"].(string)) + } + + // enable userpass auth + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + + var mountAccessor string + if auths != nil && auths["userpass/"] != nil { + mountAccessor = auths["userpass/"].Accessor + } + + // create a few entities + resp, err := client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) + if err != nil { + t.Fatal(err) + } + bobId := resp.Data["id"].(string) + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) + if err != nil { + t.Fatal(err) + } + aliceId := resp.Data["id"].(string) + + // create a few groups + resp, err = client.Logical().Write("identity/group", map[string]interface{}{ + "metadata": map[string]interface{}{"rad": true}, + "member_entity_ids": []string{aliceId}, + }) + if err != nil { + t.Fatal(err) + } + radGroupId := resp.Data["id"].(string) + + resp, err = client.Logical().Write("identity/group", map[string]interface{}{ + "metadata": map[string]interface{}{"sad": true}, + "member_entity_ids": []string{bobId}, + }) + if err != nil { + t.Fatal(err) + } + sadGroupId := resp.Data["id"].(string) + + myPath := "identity/mfa/login-enforcement/foo" + data := map[string]interface{}{ + "mfa_method_ids": []string{configIDs[0], configIDs[1]}, + "auth_method_accessors": []string{mountAccessor}, + } + + // create a login enforcement config + _, err = client.Logical().Write(myPath, data) + if err != nil { + t.Fatal(err) + } + + // read it back + resp, err = client.Logical().Read(myPath) + if err != nil { + t.Fatal(err) + } + + equal := strutil.EquivalentSlices(data["mfa_method_ids"].([]string), stringSliceFromInterfaceSlice(resp.Data["mfa_method_ids"].([]interface{}))) + if !equal { + t.Fatal("expected input mfa method ids to equal output mfa method ids") + } + equal = strutil.EquivalentSlices(data["auth_method_accessors"].([]string), stringSliceFromInterfaceSlice(resp.Data["auth_method_accessors"].([]interface{}))) + if !equal { + t.Fatal("expected input auth method accessors to equal output auth method accessors") + } + + // update it + data["identity_group_ids"] = []string{radGroupId, sadGroupId} + data["identity_entity_ids"] = []string{bobId, aliceId} + _, err = client.Logical().Write(myPath, data) + if err != nil { + t.Fatal(err) + } + + // read it back + resp, err = client.Logical().Read(myPath) + if err != nil { + t.Fatal(err) + } + + equal = strutil.EquivalentSlices(data["identity_group_ids"].([]string), stringSliceFromInterfaceSlice(resp.Data["identity_group_ids"].([]interface{}))) + if !equal { + t.Fatal("expected input identity group ids to equal output identity group ids") + } + equal = strutil.EquivalentSlices(data["identity_entity_ids"].([]string), stringSliceFromInterfaceSlice(resp.Data["identity_entity_ids"].([]interface{}))) + if !equal { + t.Fatal("expected input identity entity ids to equal output identity entity ids") + } + + // delete it + _, err = client.Logical().Delete(myPath) + if err != nil { + t.Fatal(err) + } + + // try to read it back again - should 404 + resp, err = client.Logical().Read(myPath) + + // when both the response and the error are nil on a read request, that gets translated into a 404 + if !(resp == nil && err == nil) { + t.Fatal("expected the read to 404 but it didn't") + } +} + +// TestLoginMFA_LoginEnforcement_MethodIdsIsRequired ensures that login enforcements have method ids attached +func TestLoginMFA_LoginEnforcement_MethodIdsIsRequired(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // create a login enforcement config, which should fail + _, err := client.Logical().Write("identity/mfa/login-enforcement/foo", map[string]interface{}{}) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + if !strings.Contains(err.Error(), "missing method ids") { + t.Fatal("should have received an error about missing method ids but didn't") + } +} + +// TestLoginMFA_LoginEnforcement_RequiredParameters validates that all of the required fields must be present +func TestLoginMFA_LoginEnforcement_RequiredParameters(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // first create a few configs + configIDs := make([]string, 0) + + for i := 0; i < 2; i++ { + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": fmt.Sprintf("fooCorp%d", i), + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100 + i, + }) + if err != nil { + t.Fatal(err) + } + + configIDs = append(configIDs, resp.Data["method_id"].(string)) + } + + // create a login enforcement config, which should fail + _, err := client.Logical().Write("identity/mfa/login-enforcement/foo", map[string]interface{}{ + "mfa_method_ids": []string{configIDs[0], configIDs[1]}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + if !strings.Contains(err.Error(), "One of auth_method_accessors, auth_method_types, identity_group_ids, identity_entity_ids must be specified") { + t.Fatal("expected an error about required fields but didn't get one") + } +} + +// TestLoginMFA_Method_Namespaces tests to ensure that namespace rules are followed when operating on method configs +func TestLoginMFA_Method_Namespaces(t *testing.T) { + cluster := vault.NewTestCluster(t, &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + }, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // Enable userpass authentication + err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatalf("failed to enable userpass auth: %v", err) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + mountAccessor := auths["userpass/"].Accessor + + // create a few namespaces - foo, foo/bar, foo/bar/baz, quux + _, err = client.Logical().Write("sys/namespaces/foo", nil) + if err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("sys/namespaces/quux", nil) + if err != nil { + t.Fatal(err) + } + client.SetNamespace("foo") + _, err = client.Logical().Write("sys/namespaces/bar", nil) + if err != nil { + t.Fatal(err) + } + client.SetNamespace("foo/bar") + _, err = client.Logical().Write("sys/namespaces/baz", nil) + if err != nil { + t.Fatal(err) + } + + // create a method config in ns foo/bar + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "yCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooBarMethodId := resp.Data["method_id"].(string) + fooBarPath := fmt.Sprintf("identity/mfa/method/totp/%s", fooBarMethodId) + + // create 2 additional method configs in ns foo + client.SetNamespace("foo") + resp, err = client.Logical().Write("identity/mfa/method/duo", map[string]interface{}{ + "mount_accessor": mountAccessor, + "secret_key": "oIiQkWhGZw3r5gV1cRSUQ9dwiUv4atW4vdTCx2v9", + "integration_key": "DI6XBJ2S2VEDGW8KZ2BH", + "api_hostname": "api-52ae179c.duosecurity.com", + }) + if err != nil { + t.Fatal(err) + } + + resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "aCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooMethodId := resp.Data["method_id"].(string) + + // create another method config in the root ns + client.ClearNamespace() + resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "zCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + rootMethodId := resp.Data["method_id"].(string) + rootPath := fmt.Sprintf("identity/mfa/method/totp/%s", rootMethodId) + + successCallback := func(r *api.Secret, e error) { + if e != nil { + t.Fatal(e) + } + if r != nil && r.Data["error"] != nil { + t.Fatal(r.Data["error"]) + } + } + + failureCallback := func(r *api.Secret, e error) { + if e == nil { + t.Fatal("expected to get an error but didn't get one") + } + } + + testCases := []struct { + name string + action string + namespace string + path string + data map[string]interface{} + callback func(*api.Secret, error) + }{ + { + "read foo/bar from foo/bar", + "read", + "foo/bar", + fooBarPath, + nil, + successCallback, + }, + { + "update foo/bar from foo/bar", + "update", + "foo/bar", + fooBarPath, + map[string]interface{}{"issuer": "lolCorp"}, + successCallback, + }, + { + "read foo/bar from root", + "read", + "", + fooBarPath, + nil, + successCallback, + }, + { + "update foo/bar from root", + "update", + "", + fooBarPath, + map[string]interface{}{"issuer": "lolCorp"}, + failureCallback, + }, + { + "read foo/bar from quux", + "read", + "quux", + fooBarPath, + nil, + failureCallback, + }, + { + "update foo/bar from quux", + "update", + "quux", + fooBarPath, + map[string]interface{}{"issuer": "lolCorp"}, + failureCallback, + }, + { + "read foo/bar from foo/bar/baz", + "read", + "foo/bar/baz", + fooBarPath, + nil, + successCallback, + }, + { + "update foo/bar from foo/bar/baz", + "update", + "foo/bar/baz", + fooBarPath, + map[string]interface{}{"issuer": "lolCorp"}, + failureCallback, + }, + { + "read foo/bar from foo", + "read", + "foo", + fooBarPath, + nil, + successCallback, + }, + { + "update foo/bar from foo", + "update", + "foo", + fooBarPath, + map[string]interface{}{"issuer": "lolCorp"}, + failureCallback, + }, + { + "read root from root", + "read", + "", + rootPath, + nil, + successCallback, + }, + { + "update root from root", + "update", + "", + rootPath, + map[string]interface{}{"issuer": "lolCorp"}, + successCallback, + }, + { + "read root from foo", + "read", + "foo", + rootPath, + nil, + successCallback, + }, + { + "update root from foo", + "update", + "foo", + rootPath, + map[string]interface{}{"issuer": "lolCorp"}, + failureCallback, + }, + { + "list foo/bar from foo/bar", + "list", + "foo/bar", + "identity/mfa/method/totp", + nil, + func(s *api.Secret, e error) { + if e != nil { + t.Fatal(e) + } + if s != nil && s.Data["error"] != nil { + t.Fatal(s.Data["error"]) + } + + // we should get 3 results back when listing foo/bar from foo/bar: + // one from foo/bar itself, one from foo, and one from root. + // note that there are 2 method configs defined in foo, 1 in foo/bar, 1 in root, so 4 total, + // but foo has one totp and one duo. we're listing totp here, so we should not get + // the duo one back. + if k := len(s.Data["keys"].([]interface{})); k != 3 { + t.Fatalf("expected 3 keys but got %d", k) + } + expectedKeys := []string{fooBarMethodId, fooMethodId, rootMethodId} + actualKeys := stringSliceFromInterfaceSlice(s.Data["keys"].([]interface{})) + + if !strutil.EquivalentSlices(actualKeys, expectedKeys) { + t.Fatalf("expected %v to be equivalent to %v but it wasn't", actualKeys, expectedKeys) + } + }, + }, + } + + for _, testCase := range testCases { + name := fmt.Sprintf("%s %s", testCase.action, testCase.name) + t.Run(name, func(t *testing.T) { + if testCase.namespace == "" { + client.ClearNamespace() + } else { + client.SetNamespace(testCase.namespace) + } + + var err error + var resp *api.Secret + + switch testCase.action { + case "read": + resp, err = client.Logical().Read(testCase.path) + case "update": + resp, err = client.Logical().Write(testCase.path, testCase.data) + case "list": + resp, err = client.Logical().List(testCase.path) + } + + testCase.callback(resp, err) + }) + } +} + +func TestLoginMFA_LoginEnforcement_Namespaces(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // create a few namespaces - foo, foo/bar, foo/bar/baz, quux + _, err := client.Logical().Write("sys/namespaces/foo", nil) + if err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("sys/namespaces/quux", nil) + if err != nil { + t.Fatal(err) + } + client.SetNamespace("foo") + _, err = client.Logical().Write("sys/namespaces/bar", nil) + if err != nil { + t.Fatal(err) + } + client.SetNamespace("foo/bar") + _, err = client.Logical().Write("sys/namespaces/baz", nil) + if err != nil { + t.Fatal(err) + } + + // create a method config in ns foo/bar + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "yCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooBarMethodId := resp.Data["method_id"].(string) + + // create an entity in ns foo/bar + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) + if err != nil { + t.Fatal(err) + } + aliceId := resp.Data["id"].(string) + + // create a login enforcement config in ns foo/bar + data := map[string]interface{}{ + "mfa_method_ids": []string{fooBarMethodId}, + "identity_entity_ids": []string{aliceId}, + } + + fooBarPath := "identity/mfa/login-enforcement/lol" + resp, err = client.Logical().Write(fooBarPath, data) + if err != nil { + t.Fatal(err) + } + + // create a method config in the root ns + client.ClearNamespace() + resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "zCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + rootMethodId := resp.Data["method_id"].(string) + + // create an entity in the root ns + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) + if err != nil { + t.Fatal(err) + } + bobId := resp.Data["id"].(string) + + // create another login enforcement config in the root ns + rootPath := "identity/mfa/login-enforcement/lawl" + data = map[string]interface{}{ + "mfa_method_ids": []string{rootMethodId}, + "identity_entity_ids": []string{bobId}, + } + resp, err = client.Logical().Write(rootPath, data) + if err != nil { + t.Fatal(err) + } + + testCases := []struct { + name string + action string + namespace string + path string + succeed bool + data map[string]interface{} + }{ + { + "read foo/bar from foo/bar", + "read", + "foo/bar", + fooBarPath, + true, + nil, + }, + { + "update foo/bar from foo/bar", + "update", + "foo/bar", + fooBarPath, + true, + map[string]interface{}{ + "mfa_method_ids": []string{fooBarMethodId}, + "identity_entity_ids": []string{aliceId}, + }, + }, + { + "read foo/bar from root", + "read", + "", + fooBarPath, + true, + nil, + }, + { + "update foo/bar from root", + "update", + "", + fooBarPath, + true, + map[string]interface{}{ + "mfa_method_ids": []string{rootMethodId}, + "identity_entity_ids": []string{bobId}, + }, + }, + { + "read foo/bar from quux", + "read", + "quux", + fooBarPath, + false, + nil, + }, + { + "update foo/bar from quux", + "update", + "quux", + fooBarPath, + false, + map[string]interface{}{ + "mfa_method_ids": []string{fooBarMethodId}, + "identity_entity_ids": []string{aliceId}, + }, + }, + { + "read foo/bar from foo/bar/baz", + "read", + "foo/bar/baz", + fooBarPath, + false, + nil, + }, + { + "update foo/bar from foo/bar/baz", + "update", + "foo/bar/baz", + fooBarPath, + false, + map[string]interface{}{ + "mfa_method_ids": []string{fooBarMethodId}, + "identity_entity_ids": []string{aliceId}, + }, + }, + { + "read foo/bar from foo", + "read", + "foo", + fooBarPath, + true, + nil, + }, + { + "update foo/bar from foo", + "update", + "foo", + fooBarPath, + false, + map[string]interface{}{ + "mfa_method_ids": []string{fooBarMethodId}, + "identity_entity_ids": []string{aliceId}, + }, + }, + { + "read root from root", + "read", + "", + rootPath, + true, + nil, + }, + { + "update root from root", + "update", + "", + rootPath, + true, + map[string]interface{}{ + "mfa_method_ids": []string{rootMethodId}, + "identity_entity_ids": []string{bobId}, + }, + }, + { + "read root from foo", + "read", + "foo", + rootPath, + false, + nil, + }, + { + "update root from foo", + "update", + "foo", + rootPath, + false, + map[string]interface{}{ + "mfa_method_ids": []string{rootMethodId}, + "identity_entity_ids": []string{bobId}, + }, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + if testCase.namespace == "" { + client.ClearNamespace() + } else { + client.SetNamespace(testCase.namespace) + } + + var err error + var resp *api.Secret + + switch testCase.action { + case "read": + resp, err = client.Logical().Read(testCase.path) + case "update": + resp, err = client.Logical().Write(testCase.path, testCase.data) + } + + if testCase.succeed { + if err != nil { + t.Fatal(err) + } + if resp != nil && resp.Data["error"] != nil { + t.Fatal(resp.Data["error"]) + } + } else { + if err == nil && resp != nil { + t.Fatal("expected to get an error but didn't get one") + } + } + }) + } +} + +// TestLoginMFA_LoginEnforcement_ConfigNamespaces tests that a login enforcement config should be able to access method +// ids configured in its own namespace or any of its ancestor namespaces. +func TestLoginMFA_LoginEnforcement_ConfigNamespaces(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // create a few namespaces - foo, foo/bar + _, err := client.Logical().Write("sys/namespaces/foo", nil) + if err != nil { + t.Fatal(err) + } + client.SetNamespace("foo") + _, err = client.Logical().Write("sys/namespaces/bar", nil) + if err != nil { + t.Fatal(err) + } + + // create a method config in the root ns + client.ClearNamespace() + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "rootCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + rootMethodId := resp.Data["method_id"].(string) + + // create a method config in ns foo + client.SetNamespace("foo") + resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "fooCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooMethodId := resp.Data["method_id"].(string) + + // create a method config in ns foo/bar + client.SetNamespace("foo/bar") + resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "fooBarCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooBarMethodId := resp.Data["method_id"].(string) + + // create an entity in ns foo/bar + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) + if err != nil { + t.Fatal(err) + } + bobId := resp.Data["id"].(string) + + // from the foo/bar ns, login enforcement configs should be able to reference any of the method configs + // that were created, since they're all either in foo/bar or are an ancestor of foo/bar + for _, id := range []string{rootMethodId, fooMethodId, fooBarMethodId} { + data := map[string]interface{}{ + "mfa_method_ids": []string{id}, + "identity_entity_ids": []string{bobId}, + } + + resp, err = client.Logical().Write("identity/mfa/login-enforcement/lol", data) + if err != nil { + t.Fatal(err) + } + } +} + +// TestLoginMFA_LoginEnforcement_Validation tests that all of the parameters provided to a login enforcement config +// exist within Vault and aren't just random values. +func TestLoginMFA_LoginEnforcement_Validation(t *testing.T) { + cluster := vault.NewTestCluster(t, &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + }, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // create a few namespaces - foo, bar + _, err := client.Logical().Write("sys/namespaces/foo", nil) + if err != nil { + t.Fatal(err) + } + _, err = client.Logical().Write("sys/namespaces/bar", nil) + if err != nil { + t.Fatal(err) + } + + // create a config in ns foo + client.SetNamespace("foo") + resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ + "issuer": "fooCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err != nil { + t.Fatal(err) + } + fooConfigId := resp.Data["method_id"].(string) + + // enable userpass auth in ns foo + err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ + Type: "userpass", + }) + if err != nil { + t.Fatal(err) + } + + auths, err := client.Sys().ListAuth() + if err != nil { + t.Fatal(err) + } + + var mountAccessor string + var mountType string + if auths != nil && auths["userpass/"] != nil { + mountAccessor = auths["userpass/"].Accessor + mountType = auths["userpass/"].Type + } + + // create an entity in ns foo + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) + if err != nil { + t.Fatal(err) + } + aliceId := resp.Data["id"].(string) + + // create a group in ns foo + resp, err = client.Logical().Write("identity/group", map[string]interface{}{ + "metadata": map[string]interface{}{"rad": true}, + "member_entity_ids": []string{aliceId}, + }) + if err != nil { + t.Fatal(err) + } + radGroupId := resp.Data["id"].(string) + + // create an entity in ns bar + client.SetNamespace("bar") + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) + if err != nil { + t.Fatal(err) + } + bobId := resp.Data["id"].(string) + + // create an entity in root ns + client.ClearNamespace() + resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "cynthia"}) + if err != nil { + t.Fatal(err) + } + cynthiaId := resp.Data["id"].(string) + + myPath := "identity/mfa/login-enforcement/lol" + + // try to create a login enforcement config with a non-existant method id - should fail + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{"wrong"}, + "identity_entity_ids": []string{cynthiaId}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + // try to create a login enforcement config using a method id from a different namespace that's not an ancestor + // - should fail + client.SetNamespace("bar") + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{fooConfigId}, + "identity_entity_ids": []string{bobId}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + // try to create a login enforcement config with a group id for a non-existant group - should fail + client.SetNamespace("foo") + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": [][]string{{fooConfigId}}, + "identity_group_ids": []string{"nope"}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + // try to create a login enforcement config with an entity id for a non-existant entity - should fail + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{fooConfigId}, + "identity_entity_ids": []string{"nope"}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + // try to create a login enforcement config with a non-existant auth method accessor - should fail + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{fooConfigId}, + "auth_method_accessors": []string{"wrong"}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + // try to create a login enforcement config with a non-existant auth method type - should fail + _, err = client.Logical().Write(myPath, map[string]interface{}{ + "mfa_method_ids": []string{fooConfigId}, + "auth_method_types": []string{"wrong"}, + }) + if err == nil { + t.Fatal("expected an error but didn't get one") + } + + // try to create a login enforcement config using a method id in the correct namespace with valid + // data - should succeed + data := map[string]interface{}{ + "mfa_method_ids": []string{fooConfigId}, + "identity_group_ids": []string{radGroupId}, + "identity_entity_ids": []string{aliceId}, + "auth_method_accessors": []string{mountAccessor}, + "auth_method_types": []string{mountType}, + } + _, err = client.Logical().Write(myPath, data) + if err != nil { + t.Fatal(err) + } +} + +func TestLoginMFA_UpdateNonExistentConfig(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + _, err := client.Logical().Write("mfa/method/totp/a51884c6-51f2-bdc3-f4c5-0da64fe4d061", map[string]interface{}{ + "issuer": "yCorp", + "period": 10, + "algorithm": "SHA1", + "digits": 6, + "skew": 1, + "key_size": uint(10), + "qr_size": 100, + }) + if err == nil { + t.Fatal("expected to get an error but didn't") + } + if !strings.Contains(err.Error(), "Code: 404") { + t.Fatal("expected to get a 404 but didn't") + } +} + +// This is for converting []interface{} that you know holds all strings into []string +func stringSliceFromInterfaceSlice(input []interface{}) []string { + result := make([]string, 0, len(input)) + for _, x := range input { + if val, ok := x.(string); ok { + result = append(result, val) + } + } + return result +} diff --git a/vault/identity_store.go b/vault/identity_store.go index 96643170d820..9d21b27f8c24 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -6,7 +6,7 @@ import ( "strings" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/golang/protobuf/ptypes" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-memdb" @@ -60,6 +60,7 @@ func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendCo groupUpdater: core, tokenStorer: core, entityCreator: core, + mfaBackend: core.loginMFABackend, } // Create a memdb instance, which by default, operates on lower cased @@ -134,9 +135,353 @@ func (i *IdentityStore) paths() []*framework.Path { upgradePaths(i), oidcPaths(i), oidcProviderPaths(i), + mfaPaths(i), ) } +func mfaPaths(i *IdentityStore) []*framework.Path { + return []*framework.Path{ + { + Pattern: "mfa/method/totp" + genericOptionalUUIDRegex("method_id"), + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: `The unique identifier for this MFA method.`, + }, + "issuer": { + Type: framework.TypeString, + Description: `The name of the key's issuing organization.`, + }, + "period": { + Type: framework.TypeDurationSecond, + Default: 30, + Description: `The length of time used to generate a counter for the TOTP token calculation.`, + }, + "key_size": { + Type: framework.TypeInt, + Default: 20, + Description: "Determines the size in bytes of the generated key.", + }, + "qr_size": { + Type: framework.TypeInt, + Default: 200, + Description: `The pixel size of the generated square QR code.`, + }, + "algorithm": { + Type: framework.TypeString, + Default: "SHA1", + Description: `The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512.`, + }, + "digits": { + Type: framework.TypeInt, + Default: 6, + Description: `The number of digits in the generated TOTP token. This value can either be 6 or 8.`, + }, + "skew": { + Type: framework.TypeInt, + Default: 1, + Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1.`, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodRead, + Summary: "Read the current configuration for the given MFA method", + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodTOTPUpdate, + Summary: "Update or create a configuration for the given MFA method", + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodDelete, + Summary: "Delete a configuration for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/totp/?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodListTOTP, + Summary: "List MFA method configurations for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/totp/generate$", + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: `The unique identifier for this MFA method.`, + Required: true, + }, + "entity_id": { + Type: framework.TypeString, + Description: "Entity ID on which the generated secret needs to get stored.", + Required: true, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleLoginMFAGenerateUpdate, + Summary: "Update or create TOTP secret for the given method ID on the given entity.", + }, + }, + }, + { + Pattern: "mfa/method/totp/admin-generate$", + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: `The unique identifier for this MFA method.`, + Required: true, + }, + "entity_id": { + Type: framework.TypeString, + Description: "Entity ID on which the generated secret needs to get stored.", + Required: true, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleLoginMFAAdminGenerateUpdate, + Summary: "Update or create TOTP secret for the given method ID on the given entity.", + }, + }, + }, + { + Pattern: "mfa/method/totp/admin-destroy$", + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: "The unique identifier for this MFA method.", + Required: true, + }, + "entity_id": { + Type: framework.TypeString, + Description: "Identifier of the entity from which the MFA method secret needs to be removed.", + Required: true, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleLoginMFAAdminDestroyUpdate, + Summary: "Destroys a TOTP secret for the given MFA method ID on the given entity", + }, + }, + }, + { + Pattern: "mfa/method/okta" + genericOptionalUUIDRegex("method_id"), + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: `The unique identifier for this MFA method.`, + }, + "mount_accessor": { + Type: framework.TypeString, + Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, + }, + "username_format": { + Type: framework.TypeString, + Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter. If blank, the Alias's name field will be used as-is.`, + }, + "org_name": { + Type: framework.TypeString, + Description: "Name of the organization to be used in the Okta API.", + }, + "api_token": { + Type: framework.TypeString, + Description: "Okta API key.", + }, + "base_url": { + Type: framework.TypeString, + Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`, + }, + "primary_email": { + Type: framework.TypeBool, + Description: `If true, the username will only match the primary email for the account. Defaults to false.`, + }, + "production": { + Type: framework.TypeBool, + Description: "(DEPRECATED) Use base_url instead.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodRead, + Summary: "Read the current configuration for the given MFA method", + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodOKTAUpdate, + Summary: "Update or create a configuration for the given MFA method", + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodDelete, + Summary: "Delete a configuration for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/okta/?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodListOkta, + Summary: "List MFA method configurations for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/duo" + genericOptionalUUIDRegex("method_id"), + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: `The unique identifier for this MFA method.`, + }, + "mount_accessor": { + Type: framework.TypeString, + Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, + }, + "username_format": { + Type: framework.TypeString, + Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter If blank, the Alias's name field will be used as-is. `, + }, + "secret_key": { + Type: framework.TypeString, + Description: "Secret key for Duo.", + }, + "integration_key": { + Type: framework.TypeString, + Description: "Integration key for Duo.", + }, + "api_hostname": { + Type: framework.TypeString, + Description: "API host name for Duo.", + }, + "push_info": { + Type: framework.TypeString, + Description: "Push information for Duo.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodRead, + Summary: "Read the current configuration for the given MFA method", + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodDuoUpdate, + Summary: "Update or create a configuration for the given MFA method", + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodDelete, + Summary: "Delete a configuration for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/duo/?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodListDuo, + Summary: "List MFA method configurations for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/pingid" + genericOptionalUUIDRegex("method_id"), + Fields: map[string]*framework.FieldSchema{ + "method_id": { + Type: framework.TypeString, + Description: `The unique identifier for this MFA method.`, + }, + "mount_accessor": { + Type: framework.TypeString, + Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, + }, + "username_format": { + Type: framework.TypeString, + Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: alias.name: The name returned by the mount configured via the mount_accessor parameter If blank, the Alias's name field will be used as-is. `, + }, + "settings_file_base64": { + Type: framework.TypeString, + Description: "The settings file provided by Ping, Base64-encoded. This must be a settings file suitable for third-party clients, not the PingID SDK or PingFederate.", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodRead, + Summary: "Read the current configuration for the given MFA method", + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodPingIDUpdate, + Summary: "Update or create a configuration for the given MFA method", + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodDelete, + Summary: "Delete a configuration for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/method/pingid/?$", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ListOperation: &framework.PathOperation{ + Callback: i.handleMFAMethodListPingID, + Summary: "List MFA method configurations for the given MFA method", + }, + }, + }, + { + Pattern: "mfa/login-enforcement/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name for this login enforcement configuration", + Required: true, + }, + "mfa_method_ids": { + Type: framework.TypeStringSlice, + Description: "Array of Method IDs that determine what methods will be enforced", + Required: true, + }, + "auth_method_accessors": { + Type: framework.TypeStringSlice, + Description: "Array of auth mount accessor IDs", + }, + "auth_method_types": { + Type: framework.TypeStringSlice, + Description: "Array of auth mount types", + }, + "identity_group_ids": { + Type: framework.TypeStringSlice, + Description: "Array of identity group IDs", + }, + "identity_entity_ids": { + Type: framework.TypeStringSlice, + Description: "Array of identity entity IDs", + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + Callback: i.handleMFALoginEnforcementRead, + Summary: "Read the current login enforcement", + }, + logical.UpdateOperation: &framework.PathOperation{ + Callback: i.handleMFALoginEnforcementUpdate, + Summary: "Create or update a login enforcement", + }, + logical.DeleteOperation: &framework.PathOperation{ + Callback: i.handleMFALoginEnforcementDelete, + Summary: "Delete a login enforcement", + }, + logical.ListOperation: &framework.PathOperation{ + Callback: i.handleMFALoginEnforcementList, + Summary: "List login enforcements", + }, + }, + }, + } +} + func (i *IdentityStore) initialize(ctx context.Context, req *logical.InitializationRequest) error { // Only primary should write the status if i.System().ReplicationState().HasState(consts.ReplicationPerformanceSecondary | consts.ReplicationPerformanceStandby | consts.ReplicationDRSecondary) { diff --git a/vault/identity_store_structs.go b/vault/identity_store_structs.go index 24e0f13dd387..11c7630cb030 100644 --- a/vault/identity_store_structs.go +++ b/vault/identity_store_structs.go @@ -99,6 +99,7 @@ type IdentityStore struct { groupUpdater GroupUpdater tokenStorer TokenStorer entityCreator EntityCreator + mfaBackend *LoginMFABackend } type groupDiff struct { diff --git a/vault/logical_system.go b/vault/logical_system.go index 0ff3be5726b2..bdba6d489bcf 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -25,11 +25,11 @@ import ( "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" - memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/strutil" - uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/hostutil" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/metricsutil" @@ -72,14 +72,13 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { db, _ := memdb.NewMemDB(systemBackendMemDBSchema()) b := &SystemBackend{ - Core: core, - db: db, - logger: logger, - mfaLogger: core.baseLogger.Named("mfa"), - mfaLock: &sync.RWMutex{}, + Core: core, + db: db, + logger: logger, + mfaBackend: NewPolicyMFABackend(core, logger), } - core.AddLogger(b.mfaLogger) + core.AddLogger(b.mfaBackend.mfaLogger) b.Backend = &framework.Backend{ Help: strings.TrimSpace(sysHelpRoot), @@ -147,6 +146,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { "rekey-recovery-key/init", "rekey-recovery-key/update", "rekey-recovery-key/verify", + "mfa/validate", }, LocalStorage: []string{ @@ -223,11 +223,10 @@ func (b *SystemBackend) rawPaths() []*framework.Path { // prefix. Conceptually it is similar to procfs on Linux. type SystemBackend struct { *framework.Backend - Core *Core - db *memdb.MemDB - mfaLock *sync.RWMutex - mfaLogger log.Logger - logger log.Logger + Core *Core + db *memdb.MemDB + logger log.Logger + mfaBackend *PolicyMFABackend } // handleConfigStateSanitized returns the current configuration state. The configuration diff --git a/vault/logical_system_helpers.go b/vault/logical_system_helpers.go index 3644a9ac602a..c9f03e567984 100644 --- a/vault/logical_system_helpers.go +++ b/vault/logical_system_helpers.go @@ -7,14 +7,16 @@ import ( "strings" "time" - memdb "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-memdb" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) var ( - invalidateMFAConfig = func(context.Context, *SystemBackend, string) {} + invalidateMFAConfig = func(context.Context, *SystemBackend, string) {} + invalidateLoginMFAConfig = func(context.Context, *SystemBackend, string) {} + invalidateLoginMFALoginEnforcementConfig = func(context.Context, *SystemBackend, string) {} sysInvalidate = func(b *SystemBackend) func(context.Context, string) { return nil diff --git a/vault/login_mfa.go b/vault/login_mfa.go new file mode 100644 index 000000000000..017024860936 --- /dev/null +++ b/vault/login_mfa.go @@ -0,0 +1,2798 @@ +package vault + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "image/png" + "io/ioutil" + "net/http" + "net/url" + "path" + "strings" + "sync" + "time" + + "github.com/chrismalek/oktasdk-go/okta" + "github.com/dgrijalva/jwt-go" + duoapi "github.com/duosecurity/duo_api_golang" + "github.com/duosecurity/duo_api_golang/authapi" + "github.com/golang/protobuf/proto" + "github.com/hashicorp/errwrap" + "github.com/hashicorp/go-cleanhttp" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-memdb" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault-licensing/license" + "github.com/hashicorp/vault/helper/identity" + "github.com/hashicorp/vault/helper/identity/mfa" + "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/jsonutil" + osslicense "github.com/hashicorp/vault/sdk/helper/license" + "github.com/hashicorp/vault/sdk/helper/parseutil" + "github.com/hashicorp/vault/sdk/helper/strutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault/quotas" + "github.com/mitchellh/mapstructure" + otplib "github.com/pquerna/otp" + totplib "github.com/pquerna/otp/totp" +) + +const ( + mfaMethodTypeTOTP = "totp" + mfaMethodTypeDuo = "duo" + mfaMethodTypeOkta = "okta" + mfaMethodTypePingID = "pingid" + memDBLoginMFAConfigsTable = "login_mfa_configs" + memDBMFALoginEnforcementsTable = "login_enforcements" + mfaTOTPKeysPrefix = systemBarrierPrefix + "mfa/totpkeys/" + + // loginMFAConfigPrefix is the storage prefix for persisting login MFA method + // configs + loginMFAConfigPrefix = "login-mfa/method/" + mfaLoginEnforcementPrefix = "login-mfa/enforcement/" +) + +type totpKey struct { + Key string `json:"key"` +} + +// loginMfaPaths returns the API endpoints to configure the new style +// login MFA. The following paths are supported: +// mfa/method-id/:mfa_method - management of MFA method IDs, which can be used for configuration +// mfa/login_enforcement/:config_name - configures single or two phase MFA auth +func (b *SystemBackend) loginMFAPaths() []*framework.Path { + return []*framework.Path{ + { + Pattern: "mfa/validate", + Fields: map[string]*framework.FieldSchema{ + "mfa_request_id": { + Type: framework.TypeString, + Description: "ID for this MFA request", + Required: true, + }, + "mfa_payload": { + Type: framework.TypeMap, + Description: "A map from MFA method ID to a slice of passcodes or an empty slice if the method does not use passcodes", + Required: true, + }, + }, + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.Core.loginMFABackend.handleMFALoginValidate, + Summary: "Validates the login for the given MFA methods. Upon successful validation, it returns an auth response containing the client token", + }, + }, + }, + } +} + +func genericOptionalUUIDRegex(name string) string { + return fmt.Sprintf("(/(?P<%s>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))?", name) +} + +type MFABackend struct { + Core *Core + mfaLock *sync.RWMutex + db *memdb.MemDB + mfaLogger hclog.Logger + namespacer Namespacer + methodTable string +} + +type LoginMFABackend struct { + *MFABackend +} + +func loginMFASchemaFuncs() []func() *memdb.TableSchema { + return []func() *memdb.TableSchema{ + loginMFAConfigTableSchema, + loginEnforcementTableSchema, + } +} + +func NewLoginMFABackend(core *Core, logger hclog.Logger) *LoginMFABackend { + b := NewMFABackend(core, logger, memDBLoginMFAConfigsTable, loginMFASchemaFuncs()) + return &LoginMFABackend{b} +} + +func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs []func() *memdb.TableSchema) *MFABackend { + mfaSchemas := &memdb.DBSchema{ + Tables: make(map[string]*memdb.TableSchema), + } + + for _, schemaFunc := range schemaFuncs { + schema := schemaFunc() + if _, ok := mfaSchemas.Tables[schema.Name]; ok { + panic(fmt.Sprintf("duplicate table name: %s", schema.Name)) + } + mfaSchemas.Tables[schema.Name] = schema + } + + db, _ := memdb.NewMemDB(mfaSchemas) + return &MFABackend{ + Core: core, + mfaLock: &sync.RWMutex{}, + db: db, + mfaLogger: logger.Named("mfa"), + namespacer: core, + methodTable: prefix, + } +} + +func (i *IdentityStore) handleMFAMethodListTOTP(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodList(ctx, req, d, mfaMethodTypeTOTP) +} + +func (i *IdentityStore) handleMFAMethodListDuo(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodList(ctx, req, d, mfaMethodTypeDuo) +} + +func (i *IdentityStore) handleMFAMethodListOkta(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodList(ctx, req, d, mfaMethodTypeOkta) +} + +func (i *IdentityStore) handleMFAMethodListPingID(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodList(ctx, req, d, mfaMethodTypePingID) +} + +func (i *IdentityStore) handleMFAMethodList(ctx context.Context, req *logical.Request, d *framework.FieldData, methodType string) (*logical.Response, error) { + keys, configInfo, err := i.mfaBackend.mfaMethodList(ctx, methodType) + if err != nil { + return nil, err + } + + return logical.ListResponseWithInfo(keys, configInfo), nil +} + +func (i *IdentityStore) handleMFAMethodRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + methodID := d.Get("method_id").(string) + if methodID == "" { + return logical.ErrorResponse("missing method ID"), nil + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + + respData, err := i.mfaBackend.mfaConfigReadByMethodID(methodID) + if err != nil { + return nil, err + } + + if respData == nil { + return nil, nil + } + + mfaNs, err := i.namespacer.NamespaceByID(ctx, respData["namespace_id"].(string)) + if err != nil { + return nil, err + } + + // reading the method config either from the same namespace or from the parent or from the child should all work + if !(ns.ID == mfaNs.ID || mfaNs.HasParent(ns) || ns.HasParent(mfaNs)) { + return logical.ErrorResponse("request namespace does not match method namespace"), logical.ErrPermissionDenied + } + return &logical.Response{ + Data: respData, + }, nil +} + +func (i *IdentityStore) handleMFAMethodUpdateCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, methodType string) (*logical.Response, error) { + var err error + var mConfig *mfa.Config + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + + methodID := d.Get("method_id").(string) + + b := i.mfaBackend + b.mfaLock.Lock() + defer b.mfaLock.Unlock() + + if methodID != "" { + mConfig, err = b.MemDBMFAConfigByID(methodID) + if err != nil { + return nil, err + } + + // If methodID is specified, but we didn't find anything, return a 404 + if mConfig == nil { + return nil, nil + } + } + + if mConfig == nil { + configID, err := uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("failed to generate an identifier for MFA config: %v", err) + } + mConfig = &mfa.Config{ + ID: configID, + Type: methodType, + NamespaceID: ns.ID, + } + } + + mfaNs, err := i.namespacer.NamespaceByID(ctx, mConfig.NamespaceID) + if err != nil { + return nil, err + } + + // this logic assumes that the config namespace and the current + // namespace should be the same. Note an ancestor of mfaNs is not allowed + // to create/update methodID + if ns.ID != mfaNs.ID { + return logical.ErrorResponse("request namespace does not match method namespace"), nil + } + + accessorRaw, ok := d.GetOk("mount_accessor") + if ok { + accessor := accessorRaw.(string) + validMount := i.mfaBackend.Core.router.ValidateMountByAccessor(accessor) + if validMount == nil { + return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", accessor)), nil + } + mConfig.MountAccessor = accessor + } + + mConfig.Type = methodType + usernameRaw, ok := d.GetOk("username_format") + if ok { + mConfig.UsernameFormat = usernameRaw.(string) + } + + switch methodType { + case mfaMethodTypeTOTP: + err = parseTOTPConfig(mConfig, d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + case mfaMethodTypeOkta: + if mConfig.MountAccessor == "" { + return logical.ErrorResponse(fmt.Sprintf("mfa type %q requires a %q parameter", methodType, "mount_accessor")), nil + } + err = parseOktaConfig(mConfig, d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + case mfaMethodTypeDuo: + if mConfig.MountAccessor == "" { + return logical.ErrorResponse(fmt.Sprintf("mfa type %q requires a %q parameter", methodType, "mount_accessor")), nil + } + err = parseDuoConfig(mConfig, d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + case mfaMethodTypePingID: + if mConfig.MountAccessor == "" { + return logical.ErrorResponse(fmt.Sprintf("mfa type %q requires a %q parameter", methodType, "mount_accessor")), nil + } + err = parsePingIDConfig(mConfig, d) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + + default: + return logical.ErrorResponse(fmt.Sprintf("unrecognized type %q", methodType)), nil + } + + // Store the config + err = b.putMFAConfigByID(ctx, mConfig) + if err != nil { + return nil, err + } + + // Back the config in MemDB + err = b.MemDBUpsertMFAConfig(ctx, mConfig) + if err != nil { + return nil, err + } + + if methodID == "" { + return &logical.Response{ + Data: map[string]interface{}{ + "method_id": mConfig.ID, + }, + }, nil + } else { + return nil, nil + } +} + +func (i *IdentityStore) handleMFAMethodTOTPUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypeTOTP) +} + +func (i *IdentityStore) handleMFAMethodOKTAUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypeOkta) +} + +func (i *IdentityStore) handleMFAMethodDuoUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypeDuo) +} + +func (i *IdentityStore) handleMFAMethodPingIDUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleMFAMethodUpdateCommon(ctx, req, d, mfaMethodTypePingID) +} + +func (i *IdentityStore) handleMFAMethodDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + methodID := d.Get("method_id").(string) + if methodID == "" { + return logical.ErrorResponse("missing method ID"), nil + } + return nil, i.mfaBackend.deleteMFAConfigByMethodID(ctx, methodID, memDBLoginMFAConfigsTable, loginMFAConfigPrefix) +} + +func (i *IdentityStore) handleLoginMFAGenerateUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleLoginMFAGenerateCommon(ctx, req, d.Get("method_id").(string), req.EntityID) +} + +func (i *IdentityStore) handleLoginMFAAdminGenerateUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return i.handleLoginMFAGenerateCommon(ctx, req, d.Get("method_id").(string), d.Get("entity_id").(string)) +} + +func (i *IdentityStore) handleLoginMFAGenerateCommon(ctx context.Context, req *logical.Request, methodID, entityID string) (*logical.Response, error) { + if methodID == "" { + return logical.ErrorResponse("missing method ID"), nil + } + + if entityID == "" { + return logical.ErrorResponse("missing entityID"), nil + } + + mConfig, err := i.mfaBackend.MemDBMFAConfigByID(methodID) + if err != nil { + return nil, err + } + if mConfig == nil { + return logical.ErrorResponse(fmt.Sprintf("configuration for method ID %q does not exist", methodID)), nil + } + if mConfig.ID == "" { + return nil, fmt.Errorf("configuration for method ID %q does not contain an identifier", methodID) + } + + entity, err := i.MemDBEntityByID(entityID, true) + if err != nil { + return nil, fmt.Errorf("failed to find entity with ID %q: error: %w", entityID, err) + } + + if entity == nil { + return logical.ErrorResponse("invalid entity ID"), nil + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return logical.ErrorResponse("failed to retrieve the namespace"), nil + } + if ns.ID != entity.NamespaceID { + return logical.ErrorResponse("entity namespace ID does not match the current namespace ID"), nil + } + + entityNS, err := i.namespacer.NamespaceByID(ctx, entity.NamespaceID) + if err != nil { + return logical.ErrorResponse("entity namespace not found"), nil + } + + configNS, err := i.namespacer.NamespaceByID(ctx, mConfig.NamespaceID) + if err != nil { + return logical.ErrorResponse("methodID namespace not found"), nil + } + + if configNS.ID != entityNS.ID && !entityNS.HasParent(configNS) { + return logical.ErrorResponse(fmt.Sprintf("entity namespace %s outside of the config namespace %s", entityNS.Path, configNS.Path)), nil + } + + switch mConfig.Type { + case mfaMethodTypeTOTP: + return i.mfaBackend.handleMFAGenerateTOTP(ctx, mConfig, entityID) + default: + return logical.ErrorResponse(fmt.Sprintf("generate not available for MFA type %q", mConfig.Type)), nil + } +} + +func (i *IdentityStore) handleLoginMFAAdminDestroyUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + var entity *identity.Entity + var err error + + methodID := d.Get("method_id").(string) + if methodID == "" { + return logical.ErrorResponse("missing method ID"), nil + } + + entityID := d.Get("entity_id").(string) + if entityID == "" { + return logical.ErrorResponse("missing entity ID"), nil + } + + entity, err = i.MemDBEntityByID(entityID, true) + if err != nil { + return nil, fmt.Errorf("failed to find entity with ID %q: error: %w", entityID, err) + } + + if entity == nil { + return logical.ErrorResponse("invalid entity ID"), nil + } + + mConfig, err := i.mfaBackend.MemDBMFAConfigByID(methodID) + if err != nil { + return nil, err + } + + if mConfig == nil { + return logical.ErrorResponse(fmt.Sprintf("configuration for method ID %q does not exist", methodID)), nil + } + + if mConfig.ID == "" { + return nil, fmt.Errorf("configuration for method ID %q does not contain an identifier", methodID) + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return logical.ErrorResponse("failed to retrieve the namespace"), nil + } + if ns.ID != entity.NamespaceID { + return logical.ErrorResponse("entity namespace ID does not match the current namespace ID"), nil + } + + entityNS, err := i.namespacer.NamespaceByID(ctx, entity.NamespaceID) + if err != nil { + return logical.ErrorResponse("entity namespace not found"), nil + } + + configNS, err := i.namespacer.NamespaceByID(ctx, mConfig.NamespaceID) + if err != nil { + return logical.ErrorResponse("methodID namespace not found"), nil + } + + if configNS.ID != entityNS.ID && !entityNS.HasParent(configNS) { + return logical.ErrorResponse(fmt.Sprintf("entity namespace %s outside of the current namespace %s", entityNS.Path, ns.Path)), nil + } + + // destroying the secret on the entity + if entity.MFASecrets != nil { + delete(entity.MFASecrets, mConfig.ID) + } + + err = i.upsertEntity(ctx, entity, nil, true) + if err != nil { + return nil, fmt.Errorf("failed to persist MFA secret in entity, error: %w", err) + } + + return nil, nil +} + +func (b *LoginMFABackend) handleMFALoginValidate(ctx context.Context, req *logical.Request, d *framework.FieldData) (retResp *logical.Response, retErr error) { + // mfaReqID is the ID of the login request + mfaReqID := d.Get("mfa_request_id").(string) + if mfaReqID == "" { + return logical.ErrorResponse("missing request ID"), nil + } + + // a map of methodID to passcode + methodIDToPasscodeInterface := d.Get("mfa_payload") + if methodIDToPasscodeInterface == nil { + return logical.ErrorResponse("missing mfa payload"), nil + } + + var mfaCreds logical.MFACreds + err := mapstructure.Decode(methodIDToPasscodeInterface, &mfaCreds) + if err != nil { + return logical.ErrorResponse("invalid mfa payload"), nil + } + + // getting the cached response Auth. We should note that the entry is + // removed from the queue, and if any error happens before the validation + // and creating a token succeed, we need to push the entry back to the queue. + cachedResponseAuth, err := b.Core.PopMFAResponseAuthByID(mfaReqID) + if err != nil || cachedResponseAuth == nil { + return logical.ErrorResponse("invalid request ID"), nil + } + defer func() { + // Only if retErr is NOT nil, then push back the valid entry + if retErr == nil { + return + } + pushErr := b.Core.SaveMFAResponseAuth(cachedResponseAuth) + if pushErr != nil { + retErr = multierror.Append(retErr, pushErr) + } + }() + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, fmt.Errorf("MFA validation failed. Namespace not found. error: %v", err) + } + + if ns.ID != cachedResponseAuth.RequestNSID { + return nil, fmt.Errorf("original request was issued in a different namesapce %v, current namespace is %v", cachedResponseAuth.RequestNSPath, ns.Path) + } + + entity, _, err := b.Core.fetchEntityAndDerivedPolicies(ctx, ns, cachedResponseAuth.CachedAuth.EntityID, true) + if err != nil || entity == nil { + return nil, fmt.Errorf("MFA validation failed. entity not found: %v", err) + } + + // finding the MFAEnforcement config that matches our ns. ns could be root as well + matchedMfaEnforcementList, err := b.Core.buildMFAEnforcementConfigList(ctx, entity, cachedResponseAuth.RequestPath) + if err != nil { + return nil, fmt.Errorf("failed to find MFAEnforcement configuration") + } + + if len(matchedMfaEnforcementList) == 0 { + return nil, fmt.Errorf("found nil or empty MFAEnforcement configuration") + } + + for _, eConfig := range matchedMfaEnforcementList { + err = b.Core.validateLoginMFA(ctx, eConfig, entity, req.Connection.RemoteAddr, mfaCreds) + if err != nil { + return logical.ErrorResponse("failed to satisfy enforcement %s", eConfig.Name), logical.ErrPermissionDenied + } + } + + // MFA validation has passed. Let's generate the token + resp, err := b.Core.LoginMFACreateToken(ctx, cachedResponseAuth.RequestPath, cachedResponseAuth.CachedAuth) + if err != nil { + return nil, fmt.Errorf("failed to create a token. error: %v", err) + } + + return resp, nil +} + +// LoginMFACreateToken creates a token after the login MFA is validated. +// It also applies the lease quotas on the original login request path. +func (c *Core) LoginMFACreateToken(ctx context.Context, reqPath string, cachedAuth *logical.Auth) (*logical.Response, error) { + auth := cachedAuth + resp := &logical.Response{ + Auth: auth, + } + + // Determine the source of the login + mountPoint := c.router.MatchingMount(ctx, reqPath) + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, fmt.Errorf("namespace not found: %w", err) + } + + // The request successfully authenticated itself. Run the quota checks on + // the original login request path before creating the token. + quotaResp, quotaErr := c.applyLeaseCountQuota(ctx, "as.Request{ + Path: reqPath, + MountPath: strings.TrimPrefix(mountPoint, ns.Path), + NamespacePath: ns.Path, + }) + + if quotaErr != nil { + c.logger.Error("failed to apply quota", "path", reqPath, "error", quotaErr) + return nil, quotaErr + } + + if !quotaResp.Allowed { + if c.logger.IsTrace() { + c.logger.Trace("request rejected due to lease count quota violation", "request_path", reqPath) + } + + return nil, fmt.Errorf("request path %q: %w", reqPath, quotas.ErrLeaseCountQuotaExceeded) + } + + // note that we don't need to handle the error for the following function right away. + // The function takes the response as in input variable and modify it. So, the returned + // arguments are resp and err. + leaseGenerated, resp, err := c.LoginCreateToken(ctx, ns, reqPath, mountPoint, resp) + + if quotaResp.Access != nil { + quotaAckErr := c.ackLeaseQuota(quotaResp.Access, leaseGenerated) + if quotaAckErr != nil { + err = multierror.Append(err, quotaAckErr) + } + } + + return resp, err +} + +func (i *IdentityStore) handleMFALoginEnforcementList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + return nil, nil +} + +func (i *IdentityStore) handleMFALoginEnforcementRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + respData, err := i.mfaBackend.mfaLoginEnforcementConfigByNameAndNamespace(name, ns.ID) + if err != nil { + return nil, err + } + + if respData == nil { + return nil, nil + } + + // The config is readable only from the same namespace + if ns.ID != respData["namespace_id"].(string) { + return logical.ErrorResponse("request namespace does not match method namespace"), nil + } + + return &logical.Response{ + Data: respData, + }, nil +} + +func (i *IdentityStore) handleMFALoginEnforcementUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + var err error + var eConfig *mfa.MFAEnforcementConfig + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + + name := d.Get("name").(string) + if name == "" { + return logical.ErrorResponse("missing enforcement name"), nil + } + + b := i.mfaBackend + b.mfaLock.Lock() + defer b.mfaLock.Unlock() + + eConfig, err = b.MemDBMFALoginEnforcementConfigByNameAndNamespace(name, ns.ID) + if err != nil { + return nil, err + } + + if eConfig == nil { + configID, err := uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("failed to generate an identifier for MFA login enforcement config: %w", err) + } + eConfig = &mfa.MFAEnforcementConfig{ + Name: name, + NamespaceID: ns.ID, + ID: configID, + } + } + + mfaMethodIds, ok := d.GetOk("mfa_method_ids") + if !ok { + return logical.ErrorResponse("missing method ids"), nil + } + + for _, mmid := range mfaMethodIds.([]string) { + // make sure this method id actually exists + config, err := b.mfaConfigReadByMethodID(mmid) + if err != nil { + return nil, err + } + if config == nil { + return logical.ErrorResponse("one of the provided method ids doesn't exist"), nil + } + + mfaNs, err := i.namespacer.NamespaceByID(ctx, config["namespace_id"].(string)) + if err != nil { + return logical.ErrorResponse("failed to retrieve config namespace"), nil + } + + if ns.ID != mfaNs.ID && !ns.HasParent(mfaNs) { + return logical.ErrorResponse("one of the provided method ids is in an incompatible namespace and can't be used"), nil + } + } + eConfig.MFAMethodIDs = mfaMethodIds.([]string) + + oneOfLastFour := false + authMethodAccessors, ok := d.GetOk("auth_method_accessors") + if ok { + for _, accessor := range authMethodAccessors.([]string) { + found, err := b.validateAuthEntriesForAccessorOrType(ctx, ns, func(entry *MountEntry) bool { + return accessor == entry.Accessor + }) + if err != nil { + return nil, err + } + if !found { + return logical.ErrorResponse("one of the auth method accessors provided is invalid"), nil + } + } + eConfig.AuthMethodAccessors = authMethodAccessors.([]string) + oneOfLastFour = true + } + + authMethodTypes, ok := d.GetOk("auth_method_types") + if ok { + for _, authType := range authMethodTypes.([]string) { + found, err := b.validateAuthEntriesForAccessorOrType(ctx, ns, func(entry *MountEntry) bool { + return authType == entry.Type + }) + if err != nil { + return nil, err + } + if !found { + return logical.ErrorResponse("one of the auth method types provided is invalid"), nil + } + } + eConfig.AuthMethodTypes = authMethodTypes.([]string) + oneOfLastFour = true + } + + identityGroupIds, ok := d.GetOk("identity_group_ids") + if ok { + for _, groupId := range identityGroupIds.([]string) { + group, err := i.MemDBGroupByID(groupId, true) + if err != nil { + return nil, err + } + if group == nil { + return logical.ErrorResponse("one of the provided group ids doesn't exist"), nil + } + } + eConfig.IdentityGroupIds = identityGroupIds.([]string) + oneOfLastFour = true + } + + identityEntityIds, ok := d.GetOk("identity_entity_ids") + if ok { + for _, entityId := range identityEntityIds.([]string) { + entity, err := i.MemDBEntityByID(entityId, true) + if err != nil { + return nil, err + } + if entity == nil { + return logical.ErrorResponse("one of the provided entity ids doesn't exist"), nil + } + } + eConfig.IdentityEntityIDs = identityEntityIds.([]string) + oneOfLastFour = true + } + + if !oneOfLastFour { + return logical.ErrorResponse("One of auth_method_accessors, auth_method_types, identity_group_ids, identity_entity_ids must be specified"), nil + } + + // Store the config + err = b.putMFALoginEnforcementConfig(ctx, eConfig) + if err != nil { + return nil, err + } + + // Back the config in MemDB + return nil, b.MemDBUpsertMFALoginEnforcementConfig(ctx, eConfig) +} + +func (i *IdentityStore) handleMFALoginEnforcementDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + return nil, i.mfaBackend.deleteMFALoginEnforcementConfigByNameAndNamespace(ctx, name, ns.ID) +} + +func (b *LoginMFABackend) validateAuthEntriesForAccessorOrType(ctx context.Context, ns *namespace.Namespace, validFunc func(entry *MountEntry) bool) (bool, error) { + b.Core.authLock.RLock() + defer b.Core.authLock.RUnlock() + + for _, entry := range b.Core.auth.Entries { + // only check auth methods in the current namespace + if entry.Namespace().ID != ns.ID { + continue + } + + cont, err := b.Core.checkReplicatedFiltering(ctx, entry, credentialRoutePrefix) + if err != nil { + return false, err + } + if cont { + continue + } + + if validFunc(entry) { + return true, nil + } + } + + return false, nil +} + +func (b *SystemBackend) mfaPaths() []*framework.Path { + return []*framework.Path{ + { + Pattern: "mfa/method/?", + FeatureRequired: osslicense.Features(license.FeatureMFA), + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.mfaBackend.pathMFAMethodsList, + }, + HelpSynopsis: strings.TrimSpace(mfaHelp["methods-list"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["methods-list"][1]), + }, + { + Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name") + "/generate$", + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the MFA method.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.mfaBackend.handleMFAGenerateRead, + }, + + HelpSynopsis: strings.TrimSpace(mfaHelp["totp-generate"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["totp-generate"][1]), + }, + { + Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name") + "/admin-generate$", + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the MFA method.", + }, + "entity_id": { + Type: framework.TypeString, + Description: "Entity ID on which the generated secret needs to get stored.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.mfaBackend.handleMFAAdminGenerateUpdate, + }, + HelpSynopsis: strings.TrimSpace(mfaHelp["totp-admin-generate"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["totp-admin-generate"][1]), + }, + { + Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name") + "/admin-destroy$", + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: "Name of the MFA method.", + }, + "entity_id": { + Type: framework.TypeString, + Description: "Identifier of the entity from which the MFA method secret needs to be removed.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.mfaBackend.handleMFAAdminDestroyUpdate, + }, + HelpSynopsis: strings.TrimSpace(mfaHelp["totp-admin-destroy"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["totp-admin-destroy"][1]), + }, + { + Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name"), + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: `Name of the MFA method.`, + }, + "issuer": { + Type: framework.TypeString, + Description: `The name of the key's issuing organization.`, + }, + "period": { + Type: framework.TypeDurationSecond, + Default: 30, + Description: `The length of time used to generate a counter for the TOTP token calculation.`, + }, + "key_size": { + Type: framework.TypeInt, + Default: 20, + Description: "Determines the size in bytes of the generated key.", + }, + "qr_size": { + Type: framework.TypeInt, + Default: 200, + Description: `The pixel size of the generated square QR code.`, + }, + "algorithm": { + Type: framework.TypeString, + Default: "SHA1", + Description: `The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512.`, + }, + "digits": { + Type: framework.TypeInt, + Default: 6, + Description: `The number of digits in the generated TOTP token. This value can either be 6 or 8.`, + }, + "skew": { + Type: framework.TypeInt, + Default: 1, + Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1.`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: validateRootNS(b.mfaBackend.handleTOTPConfigUpdate), + logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), + logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), + }, + + HelpSynopsis: strings.TrimSpace(mfaHelp["totp-method"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["totp-method"][1]), + }, + { + Pattern: "mfa/method/okta/" + framework.GenericNameRegex("name"), + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: `Name of the MFA method.`, + }, + "mount_accessor": { + Type: framework.TypeString, + Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, + }, + "username_format": { + Type: framework.TypeString, + Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: + +alias.name: The name returned by the mount configured via the mount_accessor parameter + +If blank, the Alias's name field will be used as-is. +`, + }, + "org_name": { + Type: framework.TypeString, + Description: "Name of the organization to be used in the Okta API.", + }, + "api_token": { + Type: framework.TypeString, + Description: "Okta API key.", + }, + "base_url": { + Type: framework.TypeString, + Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`, + }, + "primary_email": { + Type: framework.TypeBool, + Description: `If true, the username will only match the primary email for the account. Defaults to false.`, + }, + "production": { + Type: framework.TypeBool, + Description: "(DEPRECATED) Use base_url instead.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: validateRootNS(b.mfaBackend.handleOktaConfigUpdate), + logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), + logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), + }, + + HelpSynopsis: strings.TrimSpace(mfaHelp["okta-method"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["okta-method"][1]), + }, + { + Pattern: "mfa/method/duo/" + framework.GenericNameRegex("name"), + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: `Name of the MFA method.`, + }, + "mount_accessor": { + Type: framework.TypeString, + Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, + }, + "username_format": { + Type: framework.TypeString, + Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: + +alias.name: The name returned by the mount configured via the mount_accessor parameter + +If blank, the Alias's name field will be used as-is. +`, + }, + "secret_key": { + Type: framework.TypeString, + Description: "Secret key for Duo.", + }, + "integration_key": { + Type: framework.TypeString, + Description: "Integration key for Duo.", + }, + "api_hostname": { + Type: framework.TypeString, + Description: "API host name for Duo.", + }, + "push_info": { + Type: framework.TypeString, + Description: "Push information for Duo.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: validateRootNS(b.mfaBackend.handleDuoConfigUpdate), + logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), + logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), + }, + + HelpSynopsis: strings.TrimSpace(mfaHelp["duo-method"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["duo-method"][1]), + }, + { + Pattern: "mfa/method/pingid/" + framework.GenericNameRegex("name"), + FeatureRequired: osslicense.Features(license.FeatureMFA), + + Fields: map[string]*framework.FieldSchema{ + "name": { + Type: framework.TypeString, + Description: `Name of the MFA method.`, + }, + "mount_accessor": { + Type: framework.TypeString, + Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, + }, + "username_format": { + Type: framework.TypeString, + Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: + +alias.name: The name returned by the mount configured via the mount_accessor parameter + +If blank, the Alias's name field will be used as-is. +`, + }, + "settings_file_base64": { + Type: framework.TypeString, + Description: "The settings file provided by Ping, Base64-encoded. This must be a settings file suitable for third-party clients, not the PingID SDK or PingFederate.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: validateRootNS(b.mfaBackend.handlePingIDConfigUpdate), + logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), + logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), + }, + + HelpSynopsis: strings.TrimSpace(mfaHelp["pingid-method"][0]), + HelpDescription: strings.TrimSpace(mfaHelp["pingid-method"][1]), + }, + } +} + +func validateRootNS(f framework.OperationFunc) framework.OperationFunc { + return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + if ns == nil { + return nil, namespace.ErrNoNamespace + } + if ns.ID != namespace.RootNamespaceID { + return logical.ErrorResponse("this API path can only be called from the root namespace"), nil + } + return f(ctx, req, d) + } +} + +func (c *Core) PersistTOTPKey(ctx context.Context, methodID, entityID, key string) error { + ks := &totpKey{ + Key: key, + } + val, err := jsonutil.EncodeJSON(ks) + if err != nil { + return err + } + if c.barrier.Put(ctx, &logical.StorageEntry{ + Key: fmt.Sprintf("%s%s/%s", mfaTOTPKeysPrefix, methodID, entityID), + Value: val, + }); err != nil { + return err + } + return nil +} + +func (c *Core) fetchTOTPKey(ctx context.Context, methodID, entityID string) (string, error) { + entry, err := c.barrier.Get(ctx, fmt.Sprintf("%s%s/%s", mfaTOTPKeysPrefix, methodID, entityID)) + if err != nil { + return "", err + } + if entry == nil { + return "", nil + } + + ks := &totpKey{} + err = jsonutil.DecodeJSON(entry.Value, ks) + if err != nil { + return "", err + } + + return ks.Key, nil +} + +func (b *MFABackend) handleMFAGenerateTOTP(ctx context.Context, mConfig *mfa.Config, entityID string) (*logical.Response, error) { + var err error + var totpConfig *mfa.TOTPConfig + + if b.Core.identityStore == nil { + return nil, fmt.Errorf("identity store not set up, cannot service totp mfa requests") + } + + switch mConfig.Config.(type) { + case *mfa.Config_TOTPConfig: + totpConfig = mConfig.Config.(*mfa.Config_TOTPConfig).TOTPConfig + default: + return logical.ErrorResponse(fmt.Sprintf("unknown MFA config type %q", mConfig.Type)), nil + } + + b.Core.identityStore.lock.Lock() + defer b.Core.identityStore.lock.Unlock() + + // Read the entity after acquiring the lock + entity, err := b.Core.identityStore.MemDBEntityByID(entityID, true) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("failed to find entity with ID %q: {{err}}", entityID), err) + } + + if entity == nil { + return logical.ErrorResponse("invalid entity ID"), nil + } + + if entity.MFASecrets == nil { + entity.MFASecrets = make(map[string]*mfa.Secret) + } else { + _, ok := entity.MFASecrets[mConfig.ID] + if ok { + resp := &logical.Response{} + resp.AddWarning(fmt.Sprintf("Entity already has a secret for MFA method %q", mConfig.Name)) + return resp, nil + } + } + + keyObject, err := totplib.Generate(totplib.GenerateOpts{ + Issuer: totpConfig.Issuer, + AccountName: entity.ID, + Period: uint(totpConfig.Period), + Digits: otplib.Digits(totpConfig.Digits), + Algorithm: otplib.Algorithm(totpConfig.Algorithm), + SecretSize: uint(totpConfig.KeySize), + Rand: b.Core.secureRandomReader, + }) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf("failed to generate TOTP key for method name %q: {{err}}", mConfig.Name), err) + } + if keyObject == nil { + return nil, fmt.Errorf("failed to generate TOTP key for method name %q", mConfig.Name) + } + + totpURL := keyObject.String() + + totpB64Barcode := "" + if totpConfig.QRSize != 0 { + barcode, err := keyObject.Image(int(totpConfig.QRSize), int(totpConfig.QRSize)) + if err != nil { + return nil, errwrap.Wrapf("failed to generate QR code image: {{err}}", err) + } + + var buff bytes.Buffer + png.Encode(&buff, barcode) + totpB64Barcode = base64.StdEncoding.EncodeToString(buff.Bytes()) + } + + if err := b.Core.PersistTOTPKey(ctx, mConfig.ID, entity.ID, keyObject.Secret()); err != nil { + return nil, errwrap.Wrapf("failed to persist totp key: {{err}}", err) + } + + entity.MFASecrets[mConfig.ID] = &mfa.Secret{ + MethodName: mConfig.Name, + Value: &mfa.Secret_TOTPSecret{ + TOTPSecret: &mfa.TOTPSecret{ + Issuer: totpConfig.Issuer, + AccountName: entity.ID, + Period: uint32(totpConfig.Period), + Algorithm: int32(totpConfig.Algorithm), + Digits: int32(totpConfig.Digits), + Skew: uint32(totpConfig.Skew), + KeySize: uint32(totpConfig.KeySize), + }, + }, + } + + err = b.Core.identityStore.upsertEntity(ctx, entity, nil, true) + if err != nil { + return nil, errwrap.Wrapf("failed to persist MFA secret in entity: {{err}}", err) + } + + return &logical.Response{ + Data: map[string]interface{}{ + "url": totpURL, + "barcode": totpB64Barcode, + }, + }, nil +} + +func parseDuoConfig(mConfig *mfa.Config, d *framework.FieldData) error { + secretKey := d.Get("secret_key").(string) + if secretKey == "" { + return fmt.Errorf("secret_key is empty") + } + + integrationKey := d.Get("integration_key").(string) + if integrationKey == "" { + return fmt.Errorf("integration_key is empty") + } + + apiHostname := d.Get("api_hostname").(string) + if apiHostname == "" { + return fmt.Errorf("api_hostname is empty") + } + + config := &mfa.DuoConfig{ + SecretKey: secretKey, + IntegrationKey: integrationKey, + APIHostname: apiHostname, + PushInfo: d.Get("push_info").(string), + } + + mConfig.Config = &mfa.Config_DuoConfig{ + DuoConfig: config, + } + + return nil +} + +func parsePingIDConfig(mConfig *mfa.Config, d *framework.FieldData) error { + fileString := d.Get("settings_file_base64").(string) + if fileString == "" { + return fmt.Errorf("settings_file_base64 is empty") + } + + fileBytes, err := base64.StdEncoding.DecodeString(fileString) + if err != nil { + return err + } + + config := &mfa.PingIDConfig{} + for _, line := range strings.Split(string(fileBytes), "\n") { + if strings.HasPrefix(line, "#") { + continue + } + if strings.TrimSpace(line) == "" { + continue + } + splitLine := strings.SplitN(line, "=", 2) + if len(splitLine) != 2 { + return fmt.Errorf("pingid settings file contains a non-empty non-comment line that is not in key=value format: %q", line) + } + switch splitLine[0] { + case "use_base64_key": + config.UseBase64Key = splitLine[1] + case "use_signature": + result, err := parseutil.ParseBool(splitLine[1]) + if err != nil { + return errors.New("error parsing use_signature value in pingid settings file") + } + config.UseSignature = result + case "token": + config.Token = splitLine[1] + case "idp_url": + config.IDPURL = splitLine[1] + case "org_alias": + config.OrgAlias = splitLine[1] + case "admin_url": + config.AdminURL = splitLine[1] + case "authenticator_url": + config.AuthenticatorURL = splitLine[1] + default: + return fmt.Errorf("unknown key %q in pingid settings file", splitLine[0]) + } + } + + mConfig.Config = &mfa.Config_PingIDConfig{ + PingIDConfig: config, + } + + return nil +} + +func (b *LoginMFABackend) mfaConfigReadByMethodID(id string) (map[string]interface{}, error) { + mConfig, err := b.MemDBMFAConfigByID(id) + if err != nil { + return nil, err + } + if mConfig == nil { + return nil, nil + } + + return b.mfaConfigToMap(mConfig) +} + +func (b *LoginMFABackend) mfaMethodList(ctx context.Context, methodType string) ([]string, map[string]interface{}, error) { + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, nil, err + } + + ws := memdb.NewWatchSet() + txn := b.db.Txn(false) + + // get all the configs for the given type + iter, err := txn.Get(b.methodTable, "type", methodType) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch iterator for login mfa method configs in memdb: %w", err) + } + + ws.Add(iter.WatchCh()) + + var keys []string + configInfo := map[string]interface{}{} + + for { + // check for timeouts + select { + case <-ctx.Done(): + return keys, configInfo, nil + default: + break + } + + raw := iter.Next() + if raw == nil { + break + } + config := raw.(*mfa.Config) + + // return this config if it's in the same ns as the request ns OR it's in a parent ns of the request ns + mfaNs, err := b.namespacer.NamespaceByID(ctx, config.NamespaceID) + if err != nil { + return nil, nil, fmt.Errorf("failed to fetch namespace: %w", err) + } + + // the namespaces have to match, or the config namespace needs to be a parent of the request namespace + if !(ns.ID == mfaNs.ID || ns.HasParent(mfaNs)) { + continue + } + + keys = append(keys, config.ID) + configInfoEntry, err := b.mfaConfigToMap(config) + if err != nil { + return nil, nil, fmt.Errorf("failed to convert config to map: %w", err) + } + configInfo[config.ID] = configInfoEntry + } + + return keys, configInfo, nil +} + +func (b *LoginMFABackend) mfaLoginEnforcementConfigByNameAndNamespace(name, namespaceId string) (map[string]interface{}, error) { + eConfig, err := b.MemDBMFALoginEnforcementConfigByNameAndNamespace(name, namespaceId) + if err != nil { + return nil, err + } + if eConfig == nil { + return nil, nil + } + return b.mfaLoginEnforcementConfigToMap(eConfig) +} + +func (b *LoginMFABackend) mfaLoginEnforcementConfigToMap(eConfig *mfa.MFAEnforcementConfig) (map[string]interface{}, error) { + resp := make(map[string]interface{}) + resp["name"] = eConfig.Name + resp["namespace_id"] = eConfig.NamespaceID + resp["mfa_method_ids"] = append([]string{}, eConfig.MFAMethodIDs...) + resp["auth_method_accessors"] = append([]string{}, eConfig.AuthMethodAccessors...) + resp["auth_method_types"] = append([]string{}, eConfig.AuthMethodTypes...) + resp["identity_group_ids"] = append([]string{}, eConfig.IdentityGroupIds...) + resp["identity_entity_ids"] = append([]string{}, eConfig.IdentityEntityIDs...) + resp["id"] = eConfig.ID + return resp, nil +} + +func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config) (map[string]interface{}, error) { + respData := make(map[string]interface{}) + + switch mConfig.Config.(type) { + case *mfa.Config_TOTPConfig: + totpConfig := mConfig.GetTOTPConfig() + respData["issuer"] = totpConfig.Issuer + respData["period"] = totpConfig.Period + respData["digits"] = totpConfig.Digits + respData["skew"] = totpConfig.Skew + respData["key_size"] = totpConfig.KeySize + respData["qr_size"] = totpConfig.QRSize + respData["algorithm"] = otplib.Algorithm(totpConfig.Algorithm).String() + case *mfa.Config_OktaConfig: + oktaConfig := mConfig.GetOktaConfig() + respData["org_name"] = oktaConfig.OrgName + if oktaConfig.BaseURL != "" { + respData["base_url"] = oktaConfig.BaseURL + } else { + respData["production"] = oktaConfig.Production + } + respData["mount_accessor"] = mConfig.MountAccessor + respData["username_format"] = mConfig.UsernameFormat + case *mfa.Config_DuoConfig: + duoConfig := mConfig.GetDuoConfig() + respData["api_hostname"] = duoConfig.APIHostname + respData["pushinfo"] = duoConfig.PushInfo + respData["mount_accessor"] = mConfig.MountAccessor + respData["username_format"] = mConfig.UsernameFormat + case *mfa.Config_PingIDConfig: + pingConfig := mConfig.GetPingIDConfig() + respData["use_signature"] = pingConfig.UseSignature + respData["idp_url"] = pingConfig.IDPURL + respData["org_alias"] = pingConfig.OrgAlias + respData["admin_url"] = pingConfig.AdminURL + respData["authenticator_url"] = pingConfig.AuthenticatorURL + default: + return nil, fmt.Errorf("invalid method type %q was persisted, underlying type: %T", mConfig.Type, mConfig.Config) + } + + respData["type"] = mConfig.Type + respData["id"] = mConfig.ID + respData["name"] = mConfig.Name + respData["namespace_id"] = mConfig.NamespaceID + + return respData, nil +} + +func parseTOTPConfig(mConfig *mfa.Config, d *framework.FieldData) error { + if mConfig == nil { + return fmt.Errorf("config is nil") + } + + if d == nil { + return fmt.Errorf("field data is nil") + } + + algorithm := d.Get("algorithm").(string) + var keyAlgorithm otplib.Algorithm + switch algorithm { + case "SHA1": + keyAlgorithm = otplib.AlgorithmSHA1 + case "SHA256": + keyAlgorithm = otplib.AlgorithmSHA256 + case "SHA512": + keyAlgorithm = otplib.AlgorithmSHA512 + default: + return fmt.Errorf("unrecognized algorithm") + } + + digits := d.Get("digits").(int) + var keyDigits otplib.Digits + switch digits { + case 6: + keyDigits = otplib.DigitsSix + case 8: + keyDigits = otplib.DigitsEight + default: + return fmt.Errorf("digits can only be 6 or 8") + } + + period := d.Get("period").(int) + if period <= 0 { + return fmt.Errorf("period must be greater than zero") + } + + skew := d.Get("skew").(int) + switch skew { + case 0: + case 1: + default: + return fmt.Errorf("skew must be 0 or 1") + } + + keySize := d.Get("key_size").(int) + if keySize <= 0 { + return fmt.Errorf("key_size must be greater than zero") + } + + issuer := d.Get("issuer").(string) + if issuer == "" { + return fmt.Errorf("issuer must be set") + } + + 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)), + } + mConfig.Config = &mfa.Config_TOTPConfig{ + TOTPConfig: config, + } + + return nil +} + +func parseOktaConfig(mConfig *mfa.Config, d *framework.FieldData) error { + if mConfig == nil { + return errors.New("config is nil") + } + + if d == nil { + return errors.New("field data is nil") + } + + oktaConfig := &mfa.OktaConfig{} + + orgName := d.Get("org_name").(string) + if orgName == "" { + return errors.New("org_name must be set") + } + oktaConfig.OrgName = orgName + + token := d.Get("api_token").(string) + if token == "" { + return errors.New("api_token must be set") + } + oktaConfig.APIToken = token + + productionRaw, productionOk := d.GetOk("production") + if productionOk { + oktaConfig.Production = productionRaw.(bool) + } else { + oktaConfig.Production = true + } + + baseURLRaw, ok := d.GetOk("base_url") + if ok { + oktaConfig.BaseURL = baseURLRaw.(string) + } else { + // Only set if not using legacy production flag + if !productionOk { + oktaConfig.BaseURL = "okta.com" + } + } + + primaryEmailOnly := d.Get("primary_email").(bool) + if primaryEmailOnly { + oktaConfig.PrimaryEmail = true + } + + _, err := url.Parse(fmt.Sprintf("https://%s,%s", oktaConfig.OrgName, oktaConfig.BaseURL)) + if err != nil { + return errwrap.Wrapf("error parsing given base_url: {{err}}", err) + } + + mConfig.Config = &mfa.Config_OktaConfig{ + OktaConfig: oktaConfig, + } + + return nil +} + +func (c *Core) validateLoginMFA(ctx context.Context, eConfig *mfa.MFAEnforcementConfig, entity *identity.Entity, requestConnRemoteAddr string, mfaCredsMap logical.MFACreds) error { + var retErr error + for _, methodID := range eConfig.MFAMethodIDs { + // as configID is the same as methodID, and methodID is unique, we can + // use it to retrieve the MFACreds + mfaCreds, ok := mfaCredsMap[methodID] + if !ok || mfaCreds == nil { + continue + } + + err := c.validateLoginMFAInternal(ctx, methodID, entity, requestConnRemoteAddr, mfaCreds) + if err != nil { + retErr = multierror.Append(retErr, err) + continue + } + return nil + } + + return multierror.Append(retErr, fmt.Errorf("login MFA validation failed for methodID: %v", eConfig.MFAMethodIDs)) +} + +func (c *Core) validateLoginMFAInternal(ctx context.Context, methodID string, entity *identity.Entity, reqConnectionRemoteAddress string, mfaCreds []string) (retErr error) { + if entity == nil { + return fmt.Errorf("entity is nil") + } + + // Get the configuration for the MFA method set in system backend + mConfig, err := c.loginMFABackend.MemDBMFAConfigByID(methodID) + if err != nil { + return fmt.Errorf("failed to read MFA configuration") + } + + if mConfig == nil { + return fmt.Errorf("MFA method configuration not present") + } + + var alias *identity.Alias + var finalUsername string + switch mConfig.Type { + case mfaMethodTypeDuo, mfaMethodTypeOkta, mfaMethodTypePingID: + for _, entry := range entity.Aliases { + if mConfig.MountAccessor == entry.MountAccessor { + alias = entry + break + } + } + if alias == nil { + return fmt.Errorf("could not find alias in entity matching the MFA's mount accessor") + } + finalUsername = formatUsername(mConfig.UsernameFormat, alias, entity) + } + + switch mConfig.Type { + case mfaMethodTypeTOTP: + // Get the MFA secret data required to validate the supplied credentials + if entity.MFASecrets == nil { + return fmt.Errorf("MFA secret for method ID %q not present in entity %q", mConfig.ID, entity.ID) + } + entityMFASecret := entity.MFASecrets[mConfig.ID] + if entityMFASecret == nil { + return fmt.Errorf("MFA secret for method name %q not present in entity %q", mConfig.Name, entity.ID) + } + + if mfaCreds == nil { + return fmt.Errorf("MFA credentials not supplied") + } + + return c.validateTOTP(ctx, mfaCreds, entityMFASecret, mConfig.ID, entity.ID) + + case mfaMethodTypeOkta: + return c.validateOkta(ctx, mConfig, finalUsername) + + case mfaMethodTypeDuo: + return c.validateDuo(ctx, mfaCreds, mConfig, finalUsername, reqConnectionRemoteAddress) + + case mfaMethodTypePingID: + return c.validatePingID(ctx, mConfig, finalUsername) + + default: + return fmt.Errorf("unrecognized MFA type %q", mConfig.Type) + } +} + +func (c *Core) buildMFAEnforcementConfigList(ctx context.Context, entity *identity.Entity, reqPath string) ([]*mfa.MFAEnforcementConfig, error) { + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get namespace from context. %s, %v", "error", err) + } + + eConfigIter, err := c.loginMFABackend.MemDBMFALoginEnforcementConfigIterator() + if err != nil { + return nil, err + } + + me := c.router.MatchingMountEntry(ctx, reqPath) + if me == nil { + return nil, fmt.Errorf("failed to find matching mount entry for path %v", reqPath) + } + + var matchedMfaEnforcementConfig []*mfa.MFAEnforcementConfig + // finding the MFAEnforcement config that matches our ns. ns could be root as well +ECONFIG_LOOP: + for eConfigRaw := eConfigIter.Next(); eConfigRaw != nil; eConfigRaw = eConfigIter.Next() { + eConfig := eConfigRaw.(*mfa.MFAEnforcementConfig) + + // check if this config's ns applies to current req, + // i.e. is it the req's ns or an ancestor of req's ns? + eConfigNS, err := c.NamespaceByID(ctx, eConfig.NamespaceID) + if err != nil { + return nil, fmt.Errorf("failed to find the MFAEnforcementConfig namespace") + } + if eConfigNS.ID != ns.ID && !ns.HasParent(eConfigNS) { + continue + } + + // if entity is nil, an MFAEnforcementConfig could still be configured + // having mount type/accessor + if entity != nil { + if entity.NamespaceID != ns.ID { + return nil, fmt.Errorf("entity namespace ID is different than the current ns ID") + } + + // Check if entityID is in the MFAEnforcement config + if strutil.StrListContains(eConfig.IdentityEntityIDs, entity.ID) { + matchedMfaEnforcementConfig = append(matchedMfaEnforcementConfig, eConfig) + continue + } + + // Retrieve entity groups + directGroups, inheritedGroups, err := c.identityStore.groupsByEntityID(entity.ID) + if err != nil { + return nil, fmt.Errorf("error on retrieving groups by entityID in MFA") + } + for _, g := range directGroups { + if strutil.StrListContains(eConfig.IdentityGroupIds, g.ID) { + matchedMfaEnforcementConfig = append(matchedMfaEnforcementConfig, eConfig) + continue ECONFIG_LOOP + } + } + for _, g := range inheritedGroups { + if strutil.StrListContains(eConfig.IdentityGroupIds, g.ID) { + matchedMfaEnforcementConfig = append(matchedMfaEnforcementConfig, eConfig) + continue ECONFIG_LOOP + } + } + } + + for _, acc := range eConfig.AuthMethodAccessors { + if me != nil && me.Accessor == acc { + matchedMfaEnforcementConfig = append(matchedMfaEnforcementConfig, eConfig) + continue ECONFIG_LOOP + } + } + + for _, authT := range eConfig.AuthMethodTypes { + if me != nil && me.Type == authT { + matchedMfaEnforcementConfig = append(matchedMfaEnforcementConfig, eConfig) + continue ECONFIG_LOOP + } + } + } + + return matchedMfaEnforcementConfig, nil +} + +func formatUsername(format string, alias *identity.Alias, entity *identity.Entity) string { + if format == "" { + return alias.Name + } + + username := format + username = strings.Replace(username, "{{alias.name}}", alias.Name, -1) + username = strings.Replace(username, "{{entity.name}}", entity.Name, -1) + for k, v := range alias.Metadata { + username = strings.Replace(username, fmt.Sprintf("{{alias.metadata.%s}}", k), v, -1) + } + for k, v := range entity.Metadata { + username = strings.Replace(username, fmt.Sprintf("{{entity.metadata.%s}}", k), v, -1) + } + return username +} + +func (c *Core) validateDuo(ctx context.Context, creds []string, mConfig *mfa.Config, username, reqConnectionRemoteAddr string) error { + duoConfig := mConfig.GetDuoConfig() + if duoConfig == nil { + return fmt.Errorf("failed to get Duo configuration for method %q", mConfig.Name) + } + + passcode := "" + for _, cred := range creds { + if strings.HasPrefix(cred, "passcode") { + splits := strings.SplitN(cred, "=", 2) + if len(splits) != 2 { + return fmt.Errorf("invalid credential %q", cred) + } + if splits[0] == "passcode" { + passcode = splits[1] + } + } + } + + client := duoapi.NewDuoApi( + duoConfig.IntegrationKey, + duoConfig.SecretKey, + duoConfig.APIHostname, + duoConfig.PushInfo, + ) + + authClient := authapi.NewAuthApi(*client) + check, err := authClient.Check() + if err != nil { + return err + } + if check == nil { + return errors.New("Duo api check returned nil, possibly bad integration key") + } + var message string + var messageDetail string + if check.StatResult.Message != nil { + message = *check.StatResult.Message + } + if check.StatResult.Message_Detail != nil { + messageDetail = *check.StatResult.Message_Detail + } + if check.StatResult.Stat != "OK" { + return fmt.Errorf("check against Duo failed; message (if given): %q; message detail (if given): %q", message, messageDetail) + } + + preauth, err := authClient.Preauth(authapi.PreauthUsername(username), authapi.PreauthIpAddr(reqConnectionRemoteAddr)) + if err != nil { + return errwrap.Wrapf("failed to perform Duo preauth: {{err}}", err) + } + if preauth == nil { + return fmt.Errorf("failed to perform Duo preauth") + } + if preauth.StatResult.Stat != "OK" { + return fmt.Errorf("failed to perform Duo preauth: %q - %q", *preauth.StatResult.Message, *preauth.StatResult.Message_Detail) + } + + switch preauth.Response.Result { + case "allow": + return nil + case "deny": + return fmt.Errorf(preauth.Response.Status_Msg) + case "enroll": + return fmt.Errorf(fmt.Sprintf("%q - %q", preauth.Response.Status_Msg, preauth.Response.Enroll_Portal_Url)) + case "auth": + break + default: + return fmt.Errorf("invalid response from Duo preauth: %q", preauth.Response.Result) + } + + options := []func(*url.Values){} + factor := "push" + if passcode != "" { + factor = "passcode" + options = append(options, authapi.AuthPasscode(passcode)) + } else { + options = append(options, authapi.AuthDevice("auto")) + if duoConfig.PushInfo != "" { + options = append(options, authapi.AuthPushinfo(duoConfig.PushInfo)) + } + } + + options = append(options, authapi.AuthUsername(username)) + options = append(options, authapi.AuthAsync()) + + result, err := authClient.Auth(factor, options...) + if err != nil { + return errwrap.Wrapf("failed to authenticate with Duo: {{err}}", err) + } + if result.StatResult.Stat != "OK" { + return fmt.Errorf("failed to authenticate with Duo: %q - %q", *result.StatResult.Message, *result.StatResult.Message_Detail) + } + if result.Response.Txid == "" { + return fmt.Errorf("failed to get transaction ID for Duo authentication") + } + + for { + // AuthStatus does the long polling until there is a status update. So + // there is no need to wait for a second before we invoke this API. + statusResult, err := authClient.AuthStatus(result.Response.Txid) + if err != nil { + return errwrap.Wrapf("failed to get authentication status from Duo: {{err}}", err) + } + if statusResult == nil { + return errwrap.Wrapf("failed to get authentication status from Duo: {{err}}", err) + } + if statusResult.StatResult.Stat != "OK" { + return fmt.Errorf("failed to get authentication status from Duo: %q - %q", *statusResult.StatResult.Message, *statusResult.StatResult.Message_Detail) + } + + switch statusResult.Response.Result { + case "deny": + return fmt.Errorf("duo authentication failed: %q", statusResult.Response.Status_Msg) + case "allow": + return nil + } + + select { + case <-ctx.Done(): + return fmt.Errorf("duo push verification operation canceled") + case <-time.After(time.Second): + } + } +} + +func (c *Core) validateOkta(ctx context.Context, mConfig *mfa.Config, username string) error { + oktaConfig := mConfig.GetOktaConfig() + if oktaConfig == nil { + return fmt.Errorf("failed to get Okta configuration for method %q", mConfig.Name) + } + + var client *okta.Client + if oktaConfig.BaseURL != "" { + var err error + client, err = okta.NewClientWithDomain(cleanhttp.DefaultClient(), oktaConfig.OrgName, oktaConfig.BaseURL, oktaConfig.APIToken) + if err != nil { + return errwrap.Wrapf("error getting Okta client: {{err}}", err) + } + } else { + client = okta.NewClient(cleanhttp.DefaultClient(), oktaConfig.OrgName, oktaConfig.APIToken, oktaConfig.Production) + } + + var filterOpts *okta.UserListFilterOptions + if oktaConfig.PrimaryEmail { + filterOpts = &okta.UserListFilterOptions{ + EmailEqualTo: username, + } + } else { + filterOpts = &okta.UserListFilterOptions{ + LoginEqualTo: username, + } + } + + users, _, err := client.Users.ListWithFilter(filterOpts) + if err != nil { + return err + } + switch { + case len(users) == 0: + return fmt.Errorf("no users found for e-mail address") + case len(users) > 1: + return fmt.Errorf("more than one user found for e-mail address") + } + + user := &users[0] + + _, err = client.Users.PopulateMFAFactors(user) + if err != nil { + return err + } + + if len(user.MFAFactors) == 0 { + return fmt.Errorf("no MFA factors found for user") + } + + var factorID string + for _, factor := range user.MFAFactors { + if factor.FactorType == "push" { + factorID = factor.ID + break + } + } + + if factorID == "" { + return fmt.Errorf("no push-type MFA factor found for user") + } + + type pollInfo struct { + ValidationURL string `json:"href"` + } + + type pushLinks struct { + Poll pollInfo `json:"poll"` + } + + type pushResult struct { + Expiration time.Time `json:"expiresAt"` + FactorResult string `json:"factorResult"` + Links pushLinks `json:"_links"` + } + + req, err := client.NewRequest("POST", fmt.Sprintf("users/%s/factors/%s/verify", user.ID, factorID), nil) + if err != nil { + return err + } + + var result pushResult + _, err = client.Do(req, &result) + if err != nil { + return err + } + + if result.FactorResult != "WAITING" { + return fmt.Errorf("expected WAITING status for push status, got %q", result.FactorResult) + } + + for { + req, err := client.NewRequest("GET", result.Links.Poll.ValidationURL, nil) + if err != nil { + return err + } + var result pushResult + _, err = client.Do(req, &result) + if err != nil { + return err + } + switch result.FactorResult { + case "WAITING": + case "SUCCESS": + return nil + case "REJECTED": + return fmt.Errorf("push verification explicitly rejected") + case "TIMEOUT": + return fmt.Errorf("push verification timed out") + default: + return fmt.Errorf("unknown status code") + } + + select { + case <-ctx.Done(): + return fmt.Errorf("push verification operation canceled") + case <-time.After(time.Second): + } + } +} + +func (c *Core) validatePingID(ctx context.Context, mConfig *mfa.Config, username string) error { + pingConfig := mConfig.GetPingIDConfig() + if pingConfig == nil { + return fmt.Errorf("failed to get PingID configuration for method %q", mConfig.Name) + } + + signingKey, err := base64.StdEncoding.DecodeString(pingConfig.UseBase64Key) + if err != nil { + return errwrap.Wrapf("failed decoding pingid signing key: {{err}}", err) + } + + client := cleanhttp.DefaultClient() + + createRequest := func(reqPath string, reqBody map[string]interface{}) (*http.Request, error) { + // Construct the token + token := &jwt.Token{ + Method: jwt.SigningMethodHS256, + Header: map[string]interface{}{ + "alg": "HS256", + "org_alias": pingConfig.OrgAlias, + "token": pingConfig.Token, + }, + Claims: jwt.MapClaims{ + "reqHeader": map[string]interface{}{ + "locale": "en", + "orgAlias": pingConfig.OrgAlias, + "secretKey": pingConfig.Token, + "timestamp": time.Now().Format("2006-01-02 15:04:05.000"), + "version": "4.9", + }, + "reqBody": reqBody, + }, + } + signedToken, err := token.SignedString(signingKey) + if err != nil { + return nil, errwrap.Wrapf("failed signing pingid request token: {{err}}", err) + } + + // Construct the URL + if !strings.HasPrefix(reqPath, "/") { + reqPath = "/" + reqPath + } + reqURL, err := url.Parse(pingConfig.IDPURL + reqPath) + if err != nil { + return nil, errwrap.Wrapf("failed to parse pingid request url: {{err}}", err) + } + + // Construct the request; WithContext is done here since it's a shallow + // copy + req := &http.Request{} + req = req.WithContext(ctx) + req.Method = "POST" + req.URL = reqURL + req.Body = ioutil.NopCloser(bytes.NewBufferString(signedToken)) + if req.Header == nil { + req.Header = make(http.Header) + } + req.Header.Set("Content-Type", "application/json") + return req, nil + } + + do := func(req *http.Request) (*jwt.Token, error) { + // Run the request and fetch the response + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if resp == nil { + return nil, fmt.Errorf("nil response from pingid") + } + if resp.Body == nil { + return nil, fmt.Errorf("nil body in pingid response") + } + bodyBytes := bytes.NewBuffer(nil) + _, err = bodyBytes.ReadFrom(resp.Body) + resp.Body.Close() + if err != nil { + return nil, errwrap.Wrapf("error reading pingid response: {{err}}", err) + } + + // Parse the body, which is a JWT. Ensure that it's using HMAC signing + // and return the signing key in the func for validation + token, err := jwt.Parse(bodyBytes.String(), func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method %q from pingid response", token.Header["alg"]) + } + return signingKey, nil + }) + if err != nil { + return nil, errwrap.Wrapf("error parsing pingid response: {{err}}", err) + } + + // Check if parameters are as expected + if _, ok := token.Header["token"]; !ok { + return nil, fmt.Errorf("%q header not found", "token") + } + if headerTokenStr, ok := token.Header["token"].(string); !ok || headerTokenStr != pingConfig.Token { + return nil, fmt.Errorf("invalid token in ping response") + } + + // validate org alias + // This was originally 'org_alias', but it appears to now be returned as 'orgAlias'. Official + // Ping docs are sparse on the header details. We now prefer orgAlias but will still handle + // org_alias. + oa := token.Header["orgAlias"] + if oa == nil { + if oa = token.Header["org_alias"]; oa == nil { + return nil, fmt.Errorf("neither orgAlias nor org_alias headers were found") + } + } + + if headerOrgAliasStr, ok := oa.(string); !ok || headerOrgAliasStr != pingConfig.OrgAlias { + return nil, fmt.Errorf("invalid org_alias in ping response") + } + return token, nil + } + + type deviceDetails struct { + PushEnabled bool `mapstructure:"pushEnabled"` + DeviceID int64 `mapstructure:"deviceId"` + } + + type respBody struct { + SessionID string `mapstructure:"sessionId"` + ErrorID int64 `mapstructure:"errorId"` + ErrorMsg string `mapstructure:"errorMsg"` + UserDevices []deviceDetails `mapstructure:"userDevices"` + } + + type apiResponse struct { + ResponseBody respBody `mapstructure:"responseBody"` + } + + /* + // Normally we don't leave in commented code, however: + // Explicitly setting the device ID didn't work even when the device was + // push enabled (said the application was not installed on the device), so + // instead trigger default behavior, which does work, even when there's + // only one device and the deviceid matched :-/ + // We're leaving this here because if we support other types we'll likely + // still need it, and if we get device ID selection working we'll want it. + + req, err := createRequest("rest/4/startauthentication/do", map[string]interface{}{ + "spAlias": "web", + "userName": username, + }) + if err != nil { + return err + } + token, err := do(req) + if err != nil { + return err + } + + // We get back a map from the JWT library so use mapstructure + var startResp apiResponse + err = mapstructure.Decode(token.Claims, &startResp) + if err != nil { + return err + } + + // Look for at least one push-enabled method + body := startResp.ResponseBody + var foundPush bool + switch { + case body.ErrorID != 30007: + return fmt.Errorf("only pingid push authentication is currently supported") + + case len(body.UserDevices) == 0: + return fmt.Errorf("no user mfa devices returned from pingid") + + default: + for _, dev := range body.UserDevices { + if dev.PushEnabled { + foundPush = true + break + } + } + + if !foundPush { + return fmt.Errorf("no push enabled device id found from pingid") + } + } + */ + req, err := createRequest("rest/4/authonline/do", map[string]interface{}{ + "spAlias": "web", + "userName": username, + "authType": "CONFIRM", + }) + if err != nil { + return err + } + token, err := do(req) + if err != nil { + return err + } + + // Ensure a success response + var authResp apiResponse + err = mapstructure.Decode(token.Claims, &authResp) + if err != nil { + return err + } + + if authResp.ResponseBody.ErrorID != 200 { + return errors.New(authResp.ResponseBody.ErrorMsg) + } + + return nil +} + +func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSecret *mfa.Secret, configID, entityID string) error { + if len(creds) == 0 { + return fmt.Errorf("missing TOTP passcode") + } + + if len(creds) > 1 { + return fmt.Errorf("more than one TOTP passcode supplied") + } + + totpSecret := entityMethodSecret.GetTOTPSecret() + if totpSecret == nil { + return fmt.Errorf("entity does not contain the TOTP secret") + } + + key, err := c.fetchTOTPKey(ctx, configID, entityID) + if err != nil { + return errwrap.Wrapf("error fetching TOTP key: {{err}}", err) + } + + if key == "" { + return fmt.Errorf("empty key for entity's TOTP secret") + } + + validateOpts := totplib.ValidateOpts{ + Period: uint(totpSecret.Period), + Skew: uint(totpSecret.Skew), + Digits: otplib.Digits(int(totpSecret.Digits)), + Algorithm: otplib.Algorithm(int(totpSecret.Algorithm)), + } + + valid, err := totplib.ValidateCustom(creds[0], key, time.Now(), validateOpts) + if err != nil && err != otplib.ErrValidateInputInvalidLength { + return errwrap.Wrapf("failed to validate TOTP passcode: {{err}}", err) + } + + if !valid { + return fmt.Errorf("failed to validate TOTP passcode") + } + + return nil +} + +func mfaConfigTableSchema() *memdb.TableSchema { + return &memdb.TableSchema{ + Name: memDBMFAConfigsTable, + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.StringFieldIndex{ + Field: "ID", + }, + }, + "name": { + Name: "name", + Unique: true, + Indexer: &memdb.StringFieldIndex{ + Field: "Name", + }, + }, + }, + } +} + +func loginMFAConfigTableSchema() *memdb.TableSchema { + return &memdb.TableSchema{ + Name: memDBLoginMFAConfigsTable, + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.StringFieldIndex{ + Field: "ID", + }, + }, + "namespace_id": { + Name: "namespace_id", + Unique: false, + Indexer: &memdb.StringFieldIndex{ + Field: "NamespaceID", + }, + }, + "type": { + Name: "type", + Unique: false, + Indexer: &memdb.StringFieldIndex{ + Field: "Type", + }, + }, + }, + } +} + +// turns out every memdb table schema must have an id index +func loginEnforcementTableSchema() *memdb.TableSchema { + return &memdb.TableSchema{ + Name: memDBMFALoginEnforcementsTable, + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.StringFieldIndex{ + Field: "ID", + }, + }, + "nameAndNamespace": { + Name: "nameAndNamespace", + Unique: true, + Indexer: &memdb.CompoundIndex{ + Indexes: []memdb.Indexer{ + &memdb.StringFieldIndex{ + Field: "Name", + }, + &memdb.StringFieldIndex{ + Field: "NamespaceID", + }, + }, + }, + }, + }, + } +} + +func (b *MFABackend) MemDBUpsertMFAConfig(ctx context.Context, mConfig *mfa.Config) error { + txn := b.db.Txn(true) + defer txn.Abort() + + err := b.MemDBUpsertMFAConfigInTxn(txn, mConfig) + if err != nil { + return err + } + + txn.Commit() + + return nil +} + +func (b *MFABackend) MemDBUpsertMFAConfigInTxn(txn *memdb.Txn, mConfig *mfa.Config) error { + if txn == nil { + return fmt.Errorf("nil txn") + } + + if mConfig == nil { + return fmt.Errorf("config is nil") + } + + mConfigRaw, err := txn.First(b.methodTable, "id", mConfig.ID) + if err != nil { + return errwrap.Wrapf("failed to lookup MFA config from MemDB using id: {{err}}", err) + } + + if mConfigRaw != nil { + err = txn.Delete(b.methodTable, mConfigRaw) + if err != nil { + return errwrap.Wrapf("failed to delete MFA config from MemDB: {{err}}", err) + } + } + + if err := txn.Insert(b.methodTable, mConfig); err != nil { + return errwrap.Wrapf("failed to update MFA config into MemDB: {{err}}", err) + } + + return nil +} + +func (b *LoginMFABackend) MemDBUpsertMFALoginEnforcementConfig(ctx context.Context, eConfig *mfa.MFAEnforcementConfig) error { + if eConfig == nil { + return fmt.Errorf("config is nil") + } + + txn := b.db.Txn(true) + defer txn.Abort() + + eConfigRaw, err := txn.First(memDBMFALoginEnforcementsTable, "nameAndNamespace", eConfig.Name, eConfig.NamespaceID) + if err != nil { + return fmt.Errorf("failed to lookup MFA login enforcement config from MemDB using name: %w", err) + } + + if eConfigRaw != nil { + err = txn.Delete(memDBMFALoginEnforcementsTable, eConfigRaw) + if err != nil { + return fmt.Errorf("failed to delete MFA login enforcement config from MemDB: %w", err) + } + } + + if err := txn.Insert(memDBMFALoginEnforcementsTable, eConfig); err != nil { + return fmt.Errorf("failed to update MFA login enforcement config in MemDB: %w", err) + } + + txn.Commit() + return nil +} + +func (b *LoginMFABackend) MemDBMFAConfigByIDInTxn(txn *memdb.Txn, mConfigID string) (*mfa.Config, error) { + if mConfigID == "" { + return nil, fmt.Errorf("missing config id") + } + + if txn == nil { + return nil, fmt.Errorf("txn is nil") + } + + mConfigRaw, err := txn.First(b.methodTable, "id", mConfigID) + if err != nil { + return nil, errwrap.Wrapf("failed to fetch MFA config from memdb using id: {{err}}", err) + } + + if mConfigRaw == nil { + return nil, nil + } + + mConfig, ok := mConfigRaw.(*mfa.Config) + if !ok { + return nil, fmt.Errorf("failed to declare the type of fetched MFA config") + } + + return mConfig.Clone() +} + +func (b *LoginMFABackend) MemDBMFAConfigByID(mConfigID string) (*mfa.Config, error) { + if mConfigID == "" { + return nil, fmt.Errorf("missing config id") + } + + txn := b.db.Txn(false) + + return b.MemDBMFAConfigByIDInTxn(txn, mConfigID) +} + +func (b *LoginMFABackend) MemDBMFALoginEnforcementConfigByNameAndNamespace(name, namespaceId string) (*mfa.MFAEnforcementConfig, error) { + if name == "" { + return nil, fmt.Errorf("missing config name") + } + + txn := b.db.Txn(false) + defer txn.Abort() + + eConfigRaw, err := txn.First(memDBMFALoginEnforcementsTable, "nameAndNamespace", name, namespaceId) + if err != nil { + return nil, fmt.Errorf("failed to fetch MFA login enforcement config from memdb using name: %w", err) + } + + if eConfigRaw == nil { + return nil, nil + } + + eConfig, ok := eConfigRaw.(*mfa.MFAEnforcementConfig) + if !ok { + return nil, fmt.Errorf("invalid type for MFA login enforcement config in memdb") + } + + return eConfig.Clone() +} + +func (b *LoginMFABackend) MemDBMFALoginEnforcementConfigIterator() (memdb.ResultIterator, error) { + txn := b.db.Txn(false) + defer txn.Abort() + + // List all the MFAEnforcementConfigs + it, err := txn.Get(memDBMFALoginEnforcementsTable, "id") + if err != nil { + return nil, fmt.Errorf("failed to get an iterator over the MFAEnforcementConfig table: %w", err) + } + + return it, nil +} + +func (b *LoginMFABackend) deleteMFALoginEnforcementConfigByNameAndNamespace(ctx context.Context, name, namespaceId string) error { + var err error + + if name == "" { + return fmt.Errorf("missing config name") + } + + b.mfaLock.Lock() + defer b.mfaLock.Unlock() + + // delete the config from storage + eConfig, err := b.MemDBMFALoginEnforcementConfigByNameAndNamespace(name, namespaceId) + if err != nil { + return err + } + + entryIndex := mfaLoginEnforcementPrefix + eConfig.ID + barrierView := b.barrierViewForNamespace(eConfig.NamespaceID) + err = barrierView.Delete(ctx, entryIndex) + if err != nil { + return err + } + + // create a memdb transaction to delete config + txn := b.db.Txn(true) + defer txn.Abort() + + err = txn.Delete(memDBMFALoginEnforcementsTable, eConfig) + if err != nil { + return fmt.Errorf("failed to delete MFA login enforcement config from memdb: %w", err) + } + + txn.Commit() + return nil +} + +func (b *LoginMFABackend) MemDBDeleteMFALoginEnforcementConfigByNameAndNamespace(name, namespaceId, tableName string) error { + if name == "" || namespaceId == "" { + return nil + } + + txn := b.db.Txn(true) + defer txn.Abort() + + eConfig, err := b.MemDBMFALoginEnforcementConfigByNameAndNamespace(name, namespaceId) + if err != nil { + return err + } + if eConfig == nil { + return nil + } + + err = txn.Delete(memDBMFALoginEnforcementsTable, eConfig) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func (b *LoginMFABackend) deleteMFAConfigByMethodID(ctx context.Context, configID, tableName, prefix string) error { + var err error + + if configID == "" { + return fmt.Errorf("missing config id") + } + + b.mfaLock.Lock() + defer b.mfaLock.Unlock() + + // Delete the config from storage + entryIndex := prefix + configID + err = b.Core.systemBarrierView.Delete(ctx, entryIndex) + if err != nil { + return err + } + + // Create a MemDB transaction to delete config + txn := b.db.Txn(true) + defer txn.Abort() + + mConfig, err := b.MemDBMFAConfigByIDInTxn(txn, configID) + if err != nil { + return err + } + + if mConfig == nil { + return nil + } + + mfaNs, err := b.Core.NamespaceByID(ctx, mConfig.NamespaceID) + if err != nil { + return err + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return err + } + + // this logic assumes that the config namespace and the current + // namespace should be the same. Note an ancestor of mfaNs is not allowed + // to delete methodID + if ns.ID != mfaNs.ID { + return fmt.Errorf("request namespace does not match method namespace") + } + + if mConfig.Type == "totp" && mConfig.ID != "" { + // This is best effort; if they end up hanging around it's okay, they're encrypted anyways + if err := logical.ClearView(ctx, NewBarrierView(b.Core.barrier, fmt.Sprintf("%s%s", mfaTOTPKeysPrefix, mConfig.ID))); err != nil { + b.mfaLogger.Warn("unable to clear TOTP keys", "method", mConfig.Name, "error", err) + } + } + + // Delete the config from MemDB + err = b.MemDBDeleteMFAConfigByIDInTxn(txn, configID) + if err != nil { + return err + } + + txn.Commit() + + return nil +} + +func (b *LoginMFABackend) MemDBDeleteMFAConfigByID(methodId, tableName string) error { + if methodId == "" { + return nil + } + + txn := b.db.Txn(true) + defer txn.Abort() + + err := b.MemDBDeleteMFAConfigByIDInTxn(txn, methodId) + if err != nil { + return err + } + + txn.Commit() + + return nil +} + +func (b *LoginMFABackend) MemDBDeleteMFAConfigByIDInTxn(txn *memdb.Txn, configID string) error { + if configID == "" { + return nil + } + + if txn == nil { + return fmt.Errorf("txn is nil") + } + + mConfig, err := b.MemDBMFAConfigByIDInTxn(txn, configID) + if err != nil { + return err + } + + if mConfig == nil { + return nil + } + + err = txn.Delete(b.methodTable, mConfig) + if err != nil { + return errwrap.Wrapf("failed to delete MFA config from memdb: {{err}}", err) + } + + return nil +} + +func (b *LoginMFABackend) putMFAConfigByID(ctx context.Context, mConfig *mfa.Config) error { + barrierView := b.barrierViewForNamespace(mConfig.NamespaceID) + return b.putMFAConfigCommon(ctx, mConfig, loginMFAConfigPrefix, mConfig.ID, barrierView) +} + +func (b *MFABackend) putMFAConfigCommon(ctx context.Context, mConfig *mfa.Config, prefix, suffix string, barrierView *BarrierView) error { + entryIndex := prefix + suffix + marshaledEntry, err := proto.Marshal(mConfig) + if err != nil { + return err + } + + return barrierView.Put(ctx, &logical.StorageEntry{ + Key: entryIndex, + Value: marshaledEntry, + }) +} + +func (b *MFABackend) getMFAConfig(ctx context.Context, path string, barrierView *BarrierView) (*mfa.Config, error) { + entry, err := barrierView.Get(ctx, path) + if err != nil { + return nil, err + } + + if entry == nil { + return nil, nil + } + + var mConfig mfa.Config + err = proto.Unmarshal(entry.Value, &mConfig) + if err != nil { + return nil, err + } + + return &mConfig, nil +} + +func (b *LoginMFABackend) putMFALoginEnforcementConfig(ctx context.Context, eConfig *mfa.MFAEnforcementConfig) error { + entryIndex := mfaLoginEnforcementPrefix + eConfig.ID + marshaledEntry, err := proto.Marshal(eConfig) + if err != nil { + return err + } + + barrierView := b.barrierViewForNamespace(eConfig.NamespaceID) + return barrierView.Put(ctx, &logical.StorageEntry{ + Key: entryIndex, + Value: marshaledEntry, + }) +} + +func (b *LoginMFABackend) getMFALoginEnforcementConfig(ctx context.Context, key, namespaceId string) (*mfa.MFAEnforcementConfig, error) { + barrierView := b.barrierViewForNamespace(namespaceId) + entry, err := barrierView.Get(ctx, mfaLoginEnforcementPrefix+key) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var eConfig mfa.MFAEnforcementConfig + err = proto.Unmarshal(entry.Value, &eConfig) + if err != nil { + return nil, err + } + + return &eConfig, nil +} + +func (b *LoginMFABackend) barrierViewForNamespace(namespaceId string) *BarrierView { + var barrierView *BarrierView + if namespaceId == namespace.RootNamespaceID { + barrierView = b.Core.systemBarrierView + } else { + barrierView = b.Core.nsView.SubView(path.Join(namespaceId, systemBarrierPrefix) + "/") + } + + return barrierView +} + +var mfaHelp = map[string][2]string{ + "methods-list": { + "Lists all the available MFA methods by their name.", + "", + }, + "totp-generate": { + `Generates a TOTP secret for the given method name on the entity of the + calling token.`, + `This endpoint generates an MFA secret based on the + configuration tied to the method name and stores it in the entity of + the token making this request.`, + }, + "totp-admin-generate": { + `Generates a TOTP secret for the given method name on the given entity.`, + `This endpoint generates an MFA secret based on the configuration tied + to the method name and stores it in the entity corresponding to the + given entity identifier. This endpoint is used to administratively + generate TOTP secrets on entities.`, + }, + "totp-admin-destroy": { + `Deletes the TOTP secret for the given method name on the given entity.`, + `This endpoint removes the secret belonging to method name from the + entity regardless of the secret type.`, + }, + "totp-method": { + "Defines or updates a TOTP MFA method.", + "", + }, + "okta-method": { + "Defines or updates an Okta MFA method.", + "", + }, + "duo-method": { + "Defines or updates a Duo MFA method.", + "", + }, + "pingid-method": { + "Defines or updates a PingID MFA method.", + "", + }, +} diff --git a/vault/mfa_auth_resp_priority_queue.go b/vault/mfa_auth_resp_priority_queue.go new file mode 100644 index 000000000000..615b343dc345 --- /dev/null +++ b/vault/mfa_auth_resp_priority_queue.go @@ -0,0 +1,100 @@ +package vault + +import ( + "sync" + "time" + + "github.com/hashicorp/vault/sdk/queue" +) + +// NewLoginMFAPriorityQueue initializes the internal data structures and returns a new +// PriorityQueue +func NewLoginMFAPriorityQueue() *LoginMFAPriorityQueue { + pq := queue.New() + loginPQ := &LoginMFAPriorityQueue{ + wrapped: pq, + } + return loginPQ +} + +type LoginMFAPriorityQueue struct { + wrapped *queue.PriorityQueue + + // Here is a scenarios in which the lock is needed. For example, suppose + // RemoveExpiredMfaAuthResponse function pops an item to check if the item + // has been expired or not and assume that the item is still valid. Then, + // if in the meantime, an MFA validation request comes in for the same + // item, the /sys/mfa/validate endpoint will return invalid request ID + // which is not true. + l sync.RWMutex +} + +// Len returns the count of items in the Priority Queue +func (pq *LoginMFAPriorityQueue) Len() int { + pq.l.Lock() + defer pq.l.Unlock() + return pq.wrapped.Len() +} + +// Push pushes an item on to the queue. This is a wrapper/convenience +// method that calls heap.Push, so consumers do not need to invoke heap +// functions directly. Items must have unique Keys, and Items in the queue +// cannot be updated. To modify an Item, users must first remove it and re-push +// it after modifications +func (pq *LoginMFAPriorityQueue) Push(resp *MFACachedAuthResponse) error { + pq.l.Lock() + defer pq.l.Unlock() + + item := &queue.Item{ + Key: resp.RequestID, + Value: resp, + Priority: resp.TimeOfStorage.Unix(), + } + + return pq.wrapped.Push(item) +} + +// PopByKey searches the queue for an item with the given key and removes it +// from the queue if found. Returns nil if not found. +func (pq *LoginMFAPriorityQueue) PopByKey(reqID string) (*MFACachedAuthResponse, error) { + pq.l.Lock() + defer pq.l.Unlock() + + item, err := pq.wrapped.PopByKey(reqID) + if err != nil || item == nil { + return nil, err + } + + return item.Value.(*MFACachedAuthResponse), nil +} + +// RemoveExpiredMfaAuthResponse pops elements of the queue and check +// if the entry has expired or not. If the entry has not expired, it pushes +// back the entry to the queue. It returns false if there is no expired element +// left to be removed, true otherwise. +// cutoffTime should normally be time.Now() except for tests. +func (pq *LoginMFAPriorityQueue) RemoveExpiredMfaAuthResponse(expiryTime time.Duration, cutoffTime time.Time) error { + pq.l.Lock() + defer pq.l.Unlock() + + item, err := pq.wrapped.Pop() + if err != nil && err != queue.ErrEmpty { + return err + } + if err == queue.ErrEmpty { + return nil + } + + mfaResp := item.Value.(*MFACachedAuthResponse) + + storageTime := mfaResp.TimeOfStorage + if cutoffTime.Before(storageTime.Add(expiryTime)) { + // the highest priority entry has not been expired yet, pushing it + // back and return + err := pq.wrapped.Push(item) + if err != nil { + return err + } + } + return nil +} diff --git a/vault/mfa_auth_resp_priority_queue_test.go b/vault/mfa_auth_resp_priority_queue_test.go new file mode 100644 index 000000000000..b8930158b508 --- /dev/null +++ b/vault/mfa_auth_resp_priority_queue_test.go @@ -0,0 +1,109 @@ +package vault + +import ( + "testing" + "time" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/sdk/queue" +) + +// some tests rely on the ordering of items from this method +func testCases() (tc []*MFACachedAuthResponse) { + // create a slice of items with times offest by these seconds + for _, m := range []time.Duration{ + 5, + 183600, // 51 hours + 15, // 15 seconds + 45, // 45 seconds + 900, // 15 minutes + 360, // 6 minutes + 7200, // 2 hours + 183600, // 51 hours + 7201, // 2 hours, 1 second + 115200, // 32 hours + 1209600, // 2 weeks + } { + n := time.Now() + ft := n.Add(time.Second * m) + uid, err := uuid.GenerateUUID() + if err != nil { + continue + } + tc = append(tc, &MFACachedAuthResponse{ + TimeOfStorage: ft, + RequestID: uid, + }) + } + return +} + +func TestLoginMFAPriorityQueue_PushPopByKey(t *testing.T) { + pq := NewLoginMFAPriorityQueue() + + if pq.Len() != 0 { + t.Fatalf("expected new queue to have zero size, got (%d)", pq.Len()) + } + + tc := testCases() + tcl := len(tc) + for _, i := range tc { + if err := pq.Push(i); err != nil { + t.Fatal(err) + } + } + + if pq.Len() != tcl { + t.Fatalf("error adding items, expected (%d) items, got (%d)", tcl, pq.Len()) + } + + item, err := pq.PopByKey(tc[0].RequestID) + if err != nil { + t.Fatalf("error popping item: %s", err) + } + if tc[0].TimeOfStorage != item.TimeOfStorage { + t.Fatalf("expected tc[0] and popped item to match, got (%v) and (%v)", tc[0].TimeOfStorage, item.TimeOfStorage) + } + + // push item with duplicate key + dErr := pq.Push(tc[1]) + if dErr != queue.ErrDuplicateItem { + t.Fatal(err) + } + // push item with no key + tc[2].RequestID = "" + kErr := pq.Push(tc[2]) + if kErr != nil && kErr.Error() != "error adding item: Item Key is required" { + t.Fatal(kErr) + } + + // check nil,nil error for not found + i, err := pq.PopByKey("empty") + if err != nil && i != nil { + t.Fatalf("expected nil error for PopByKey of non-existing key, got: %s", err) + } +} + +func TestLoginMFARemoveStaleEntries(t *testing.T) { + pq := NewLoginMFAPriorityQueue() + + tc := testCases() + for _, i := range tc { + if err := pq.Push(i); err != nil { + t.Fatal(err) + } + } + + cutoffTime := time.Now().Add(371 * time.Second) + timeout := time.Now().Add(5 * time.Second) + for { + if time.Now().After(timeout) { + break + } + pq.RemoveExpiredMfaAuthResponse(defaultMFAAuthResponseTTL, cutoffTime) + } + + if pq.Len() != 8 { + t.Fatalf("failed to remove %d stale entries", pq.Len()) + } +} diff --git a/vault/request_forwarding_service.pb.go b/vault/request_forwarding_service.pb.go index 62962be0c670..dc558d5caabe 100644 --- a/vault/request_forwarding_service.pb.go +++ b/vault/request_forwarding_service.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc v3.19.3 // source: vault/request_forwarding_service.proto package vault diff --git a/vault/request_handling.go b/vault/request_handling.go index e1a4915f1e53..06203d2d3e9a 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -7,12 +7,14 @@ import ( "strings" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/hashicorp/errwrap" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/strutil" - sockaddr "github.com/hashicorp/go-sockaddr" + "github.com/hashicorp/go-sockaddr" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/identity" + "github.com/hashicorp/vault/helper/identity/mfa" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/internalshared/configutil" @@ -1073,7 +1075,7 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp // Only the token store is allowed to return an auth block, for any // other request this is an internal error. if resp != nil && resp.Auth != nil { - if !strings.HasPrefix(req.Path, "auth/token/") { + if !strings.HasPrefix(req.Path, "auth/token/") && req.Path != "sys/mfa/validate" { c.logger.Error("unexpected Auth response for non-token backend", "request_path", req.Path) retErr = multierror.Append(retErr, ErrInternalError) return nil, auth, retErr @@ -1399,94 +1401,104 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re CREATE_TOKEN: // Determine the source of the login source := c.router.MatchingMount(ctx, req.Path) - source = strings.TrimPrefix(source, credentialRoutePrefix) - source = strings.Replace(source, "/", "-", -1) - // Prepend the source to the display name - auth.DisplayName = strings.TrimSuffix(source+auth.DisplayName, "-") - - sysView := c.router.MatchingSystemView(ctx, req.Path) - if sysView == nil { - c.logger.Error("unable to look up sys view for login path", "request_path", req.Path) - return nil, nil, ErrInternalError - } - - tokenTTL, warnings, err := framework.CalculateTTL(sysView, 0, auth.TTL, auth.Period, auth.MaxTTL, auth.ExplicitMaxTTL, time.Time{}) - if err != nil { - return nil, nil, err - } - for _, warning := range warnings { - resp.AddWarning(warning) - } - - _, identityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, ns, auth.EntityID, false) + // Login MFA + entity, _, err := c.fetchEntityAndDerivedPolicies(ctx, ns, auth.EntityID, false) if err != nil { return nil, nil, ErrInternalError } - - auth.TokenPolicies = policyutil.SanitizePolicies(auth.Policies, !auth.NoDefaultPolicy) - allPolicies := policyutil.SanitizePolicies(append(auth.TokenPolicies, identityPolicies[ns.ID]...), policyutil.DoNotAddDefaultPolicy) - - // Prevent internal policies from being assigned to tokens. We check - // this on auth.Policies including derived ones from Identity before - // actually making the token. - for _, policy := range allPolicies { - if policy == "root" { - return logical.ErrorResponse("auth methods cannot create root tokens"), nil, logical.ErrInvalidRequest - } - if strutil.StrListContains(nonAssignablePolicies, policy) { - return logical.ErrorResponse(fmt.Sprintf("cannot assign policy %q", policy)), nil, logical.ErrInvalidRequest + // finding the MFAEnforcementConfig that matches the ns and either of + // entityID, MountAccessor, GroupID, or Auth type. + matchedMfaEnforcementList, err := c.buildMFAEnforcementConfigList(ctx, entity, req.Path) + if err != nil { + return nil, nil, fmt.Errorf("failed to find MFAEnforcement configuration, error: %v", err) + } + + // (for the context, a response warning above says: "primary cluster + // doesn't yet issue entities for local auth mounts; falling back + // to not issuing entities for local auth mounts") + // based on the above, if the entity is nil, check if MFAEnforcementConfig + // is configured or not. If not, continue as usual, but if there + // is something, then report an error indicating that the user is not + // allowed to login because there is no entity associated with it. + // This is because an entity is needed to enforce MFA. + if entity == nil && len(matchedMfaEnforcementList) > 0 { + // this logic means that an MFAEnforcementConfig was configured with + // only mount type or mount accessor + return nil, nil, logical.ErrPermissionDenied + } + + // The resp.Auth has been populated with the information that is required for MFA validation + // This is why, the MFA check is placed at this point. The resp.Auth is going to be fully cached + // in memory so that it would be used to return to the user upon MFA validation is completed. + if entity != nil { + if len(matchedMfaEnforcementList) == 0 && len(req.MFACreds) > 0 { + resp.AddWarning("Found MFA header but failed to find MFA Enforcement Config") } - } - var registerFunc RegisterAuthFunc - var funcGetErr error - // Batch tokens should not be forwarded to perf standby - if auth.TokenType == logical.TokenTypeBatch { - registerFunc = c.RegisterAuth - } else { - registerFunc, funcGetErr = getAuthRegisterFunc(c) - } - if funcGetErr != nil { - retErr = multierror.Append(retErr, funcGetErr) - return nil, auth, retErr - } + // If X-Vault-MFA header is supplied to the login request, + // run single-phase login MFA check, else run two-phase login MFA check + if len(matchedMfaEnforcementList) > 0 && len(req.MFACreds) > 0 { + for _, eConfig := range matchedMfaEnforcementList { + err = c.validateLoginMFA(ctx, eConfig, entity, req.Connection.RemoteAddr, req.MFACreds) + if err != nil { + return nil, nil, logical.ErrPermissionDenied + } + } + } else if len(matchedMfaEnforcementList) > 0 && len(req.MFACreds) == 0 { + mfaRequestID, err := uuid.GenerateUUID() + if err != nil { + return nil, nil, err + } + // sending back the MFARequirement config + mfaRequirement := &logical.MFARequirement{ + MFARequestID: mfaRequestID, + MFAConstraints: make(map[string]*logical.MFAConstraintAny), + } + for _, eConfig := range matchedMfaEnforcementList { + mfaAny, err := c.buildMfaEnforcementResponse(eConfig) + if err != nil { + return nil, nil, err + } + mfaRequirement.MFAConstraints[eConfig.Name] = mfaAny + } - err = registerFunc(ctx, tokenTTL, req.Path, auth) - switch { - case err == nil: - if auth.TokenType != logical.TokenTypeBatch { - leaseGenerated = true + // for two phased MFA enforcement, we should not return the regular auth + // response. This flag is indicate to store the auth response for later + // and return MFARequirement only + respAuth := &MFACachedAuthResponse{ + CachedAuth: resp.Auth, + RequestPath: req.Path, + RequestNSID: ns.ID, + RequestNSPath: ns.Path, + RequestConnRemoteAddr: req.Connection.RemoteAddr, // this is needed for the DUO method + TimeOfStorage: time.Now(), + RequestID: req.ID, + } + err = c.SaveMFAResponseAuth(respAuth) + if err != nil { + return nil, nil, err + } + auth = nil + resp.Auth = &logical.Auth{ + MFARequirement: mfaRequirement, + } + // going to return early before generating the token + // the user receives the mfaRequirement, and need to use the + // login MFA validate endpoint to get the token + return resp, auth, retErr } - case err == ErrInternalError: - return nil, auth, err - default: - return logical.ErrorResponse(err.Error()), auth, logical.ErrInvalidRequest } - auth.IdentityPolicies = policyutil.SanitizePolicies(identityPolicies[ns.ID], policyutil.DoNotAddDefaultPolicy) - delete(identityPolicies, ns.ID) - auth.ExternalNamespacePolicies = identityPolicies - auth.Policies = allPolicies - // Attach the display name, might be used by audit backends req.DisplayName = auth.DisplayName - // Count the successful token creation - ttl_label := metricsutil.TTLBucket(tokenTTL) - // Do not include namespace path in mount point; already present as separate label. - mountPointWithoutNs := ns.TrimmedPath(req.MountPoint) - c.metricSink.IncrCounterWithLabels( - []string{"token", "creation"}, - 1, - []metrics.Label{ - metricsutil.NamespaceLabel(ns), - {"auth_method", req.MountType}, - {"mount_point", mountPointWithoutNs}, - {"creation_ttl", ttl_label}, - {"token_type", auth.TokenType.String()}, - }, - ) + leaseGen, respTokenCreate, errCreateToken := c.LoginCreateToken(ctx, ns, req.Path, source, resp) + leaseGenerated = leaseGen + if errCreateToken != nil { + return respTokenCreate, nil, errCreateToken + } + resp = respTokenCreate } // if we were already going to return some error from this login, do that. @@ -1509,6 +1521,126 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re return resp, auth, routeErr } +// LoginCreateToken creates a token as a result of a login request. +// If MFA is enforced, mfa/validate endpoint calls this functions +// after successful MFA validation to generate the token. +func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, reqPath, mountPoint string, resp *logical.Response) (bool, *logical.Response, error) { + auth := resp.Auth + + source := strings.TrimPrefix(mountPoint, credentialRoutePrefix) + source = strings.Replace(source, "/", "-", -1) + + // Prepend the source to the display name + auth.DisplayName = strings.TrimSuffix(source+auth.DisplayName, "-") + + // Determine mount type + mountEntry := c.router.MatchingMountEntry(ctx, reqPath) + if mountEntry == nil { + return false, nil, fmt.Errorf("failed to find a matching mount") + } + + sysView := c.router.MatchingSystemView(ctx, reqPath) + if sysView == nil { + c.logger.Error("unable to look up sys view for login path", "request_path", reqPath) + return false, nil, ErrInternalError + } + + tokenTTL, warnings, err := framework.CalculateTTL(sysView, 0, auth.TTL, auth.Period, auth.MaxTTL, auth.ExplicitMaxTTL, time.Time{}) + if err != nil { + return false, nil, err + } + for _, warning := range warnings { + resp.AddWarning(warning) + } + + _, identityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, ns, auth.EntityID, false) + if err != nil { + return false, nil, ErrInternalError + } + + auth.TokenPolicies = policyutil.SanitizePolicies(auth.Policies, !auth.NoDefaultPolicy) + allPolicies := policyutil.SanitizePolicies(append(auth.TokenPolicies, identityPolicies[ns.ID]...), policyutil.DoNotAddDefaultPolicy) + + // Prevent internal policies from being assigned to tokens. We check + // this on auth.Policies including derived ones from Identity before + // actually making the token. + for _, policy := range allPolicies { + if policy == "root" { + return false, logical.ErrorResponse("auth methods cannot create root tokens"), logical.ErrInvalidRequest + } + if strutil.StrListContains(nonAssignablePolicies, policy) { + return false, logical.ErrorResponse(fmt.Sprintf("cannot assign policy %q", policy)), logical.ErrInvalidRequest + } + } + + var registerFunc RegisterAuthFunc + var funcGetErr error + // Batch tokens should not be forwarded to perf standby + if auth.TokenType == logical.TokenTypeBatch { + registerFunc = c.RegisterAuth + } else { + registerFunc, funcGetErr = getAuthRegisterFunc(c) + } + if funcGetErr != nil { + return false, nil, funcGetErr + } + + leaseGenerated := false + err = registerFunc(ctx, tokenTTL, reqPath, auth) + switch { + case err == nil: + if auth.TokenType != logical.TokenTypeBatch { + leaseGenerated = true + } + case err == ErrInternalError: + return false, nil, err + default: + return false, logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + + auth.IdentityPolicies = policyutil.SanitizePolicies(identityPolicies[ns.ID], policyutil.DoNotAddDefaultPolicy) + delete(identityPolicies, ns.ID) + auth.ExternalNamespacePolicies = identityPolicies + auth.Policies = allPolicies + + // Count the successful token creation + ttl_label := metricsutil.TTLBucket(tokenTTL) + // Do not include namespace path in mount point; already present as separate label. + mountPointWithoutNs := ns.TrimmedPath(mountPoint) + c.metricSink.IncrCounterWithLabels( + []string{"token", "creation"}, + 1, + []metrics.Label{ + metricsutil.NamespaceLabel(ns), + {"auth_method", mountEntry.Type}, + {"mount_point", mountPointWithoutNs}, + {"creation_ttl", ttl_label}, + {"token_type", auth.TokenType.String()}, + }, + ) + + return leaseGenerated, resp, nil +} + +func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*logical.MFAConstraintAny, error) { + mfaAny := &logical.MFAConstraintAny{ + Any: []*logical.MFAMethodID{}, + } + for _, methodID := range eConfig.MFAMethodIDs { + mConfig, err := c.loginMFABackend.MemDBMFAConfigByID(methodID) + if err != nil { + return nil, fmt.Errorf("failed to get methodID %s from MFA config table, error: %v", methodID, err) + } + mfaMethod := &logical.MFAMethodID{ + Type: mConfig.Type, + ID: methodID, + UsesPasscode: mConfig.Type == mfaMethodTypeTOTP || mConfig.Type == mfaMethodTypeDuo, + } + mfaAny.Any = append(mfaAny.Any, mfaMethod) + } + return mfaAny, nil +} + func blockRequestIfErrorImpl(_ *Core, _, _ string) error { return nil } // RegisterAuth uses a logical.Auth object to create a token entry in the token From 66ee05628f3f5418707ef6e698b78a294b29f0d5 Mon Sep 17 00:00:00 2001 From: hghaf099 <83242695+hghaf099@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:37:32 -0500 Subject: [PATCH 02/17] ENT OSS segragation (#14088) --- go.mod | 1 + go.sum | 10 +- vault/core_util.go | 12 + .../identity/login_mfa_totp_test.go | 629 +----------- vault/external_tests/mfa/login_mfa_test.go | 946 ------------------ vault/identity_store_oss.go | 4 - vault/logical_system.go | 7 +- vault/login_mfa.go | 344 +------ vault/request_handling.go | 2 +- 9 files changed, 43 insertions(+), 1912 deletions(-) diff --git a/go.mod b/go.mod index 73b048527843..5b1e29e94483 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/coreos/go-semver v0.3.0 github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/denisenkom/go-mssqldb v0.12.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/docker/docker v20.10.10+incompatible github.com/docker/go-connections v0.4.0 github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 diff --git a/go.sum b/go.sum index 654db472105b..81656619c4d1 100644 --- a/go.sum +++ b/go.sum @@ -452,6 +452,7 @@ github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8l github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba h1:p6poVbjHDkKa+wtC8frBMwQtT3BmqGYBjzMwJ63tuR4= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/digitalocean/godo v1.7.5 h1:JOQbAO6QT1GGjor0doT0mXefX2FgUDPOpYh2RaXA+ko= @@ -791,8 +792,6 @@ github.com/hashicorp/cap v0.1.1/go.mod h1:VfBvK2ULRyqsuqAnjgZl7HJ7/CGMC7ro4H5eXi github.com/hashicorp/consul-template v0.27.2-0.20211014231529-4ff55381f1c4 h1:Heoq6IaSKwqOzAJMDg33LRu0GmNxVswQkIcREBFQD2E= github.com/hashicorp/consul-template v0.27.2-0.20211014231529-4ff55381f1c4/go.mod h1:cAi5bOqno7Ao5sFHu7O80wMOPnqcF5ADrTApWU4Lqx4= github.com/hashicorp/consul/api v1.4.0/go.mod h1:xc8u05kyMa3Wjr9eEAsIAo3dg8+LywT5E/Cl7cNS5nU= -github.com/hashicorp/consul/api v1.11.0 h1:Hw/G8TtRvOElqxVIhBzXciiSTbapq8hZ2XKZsXk5ZCE= -github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.12.0 h1:k3y1FYv6nuKyNTqj6w9gXOx5r5CfLj/k/euUeBXj1OY= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -911,12 +910,10 @@ github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+C github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/mdns v1.0.1 h1:XFSOubp8KWB+Jd2PDyaX5xUd5bhSP/+pTDZVDMzZJM8= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/mdns v1.0.4 h1:sY0CMhFmjIPDMlTB+HfymFHCaYLhgifZ0QhjaYKD/UQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= @@ -938,8 +935,6 @@ github.com/hashicorp/raft-snapshot v1.0.3 h1:lTgBBGMFcuKBTwHqWZ4r0TLzNsqo/OByCga github.com/hashicorp/raft-snapshot v1.0.3/go.mod h1:5sL9eUn72lH5DzsFIJ9jaysITbHksSSszImWSOTC8Ic= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.4/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/vault-plugin-auth-alicloud v0.10.0 h1:ujwHy67QeSwIWN2OLw4K/9ImcZaNU2jeNpWDI17/aQk= @@ -1155,8 +1150,6 @@ github.com/michaelklishin/rabbit-hole/v2 v2.11.0 h1:v/Jtrr0FY82pITY3VFhIDaXCllPC github.com/michaelklishin/rabbit-hole/v2 v2.11.0/go.mod h1:tVpCFikY4BB40a436H81PRVybvtNwFwWI3oCflUTec8= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA= -github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -1953,7 +1946,6 @@ golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= diff --git a/vault/core_util.go b/vault/core_util.go index 5200c461d8fe..cf48c63ab3f8 100644 --- a/vault/core_util.go +++ b/vault/core_util.go @@ -4,7 +4,9 @@ package vault import ( "context" + "fmt" + "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/command/server" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/license" @@ -59,6 +61,16 @@ func (c *Core) setupReplicationResolverHandler() error { return nil } +func NewPolicyMFABackend(core *Core, logger hclog.Logger) *PolicyMFABackend { return nil } + +func (c *Core) barrierViewForNamespace(namespaceId string) (*BarrierView, error) { + if namespaceId != namespace.RootNamespaceID { + return nil, fmt.Errorf("faild to find barrier view for non-root namespace") + } + + return c.systemBarrierView, nil +} + // GetCoreConfigInternal returns the server configuration // in struct format. func (c *Core) GetCoreConfigInternal() *server.Config { diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index 6d42fb6dd6e9..ebb17c2d7203 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -2,14 +2,11 @@ package identity import ( "fmt" - "strings" "testing" - "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/credential/userpass" "github.com/hashicorp/vault/builtin/logical/totp" - "github.com/hashicorp/vault/helper/testhelpers" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" @@ -24,22 +21,6 @@ var loginMFACoreConfig = &vault.CoreConfig{ }, } -type totpCode struct { - name string - methodID string - namespacePath string // this is tied to the entityID or the mount accessor - entityID string -} - -func getNamespaceSpecificMountAccessor(namespace string, client *api.Client, t *testing.T) string { - client.SetNamespace(namespace) - auths, err := client.Sys().ListAuth() - if err != nil || auths == nil || auths["userpass/"] == nil { - t.Fatalf("failed to get the list of auths") - } - return auths["userpass/"].Accessor -} - func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { cluster := vault.NewTestCluster(t, loginMFACoreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, @@ -132,7 +113,7 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { // login MFA { // create a config - resp1, err := client.Logical().Write("identity/mfa/method-id/totp", map[string]interface{}{ + resp1, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ "issuer": "yCorp", "period": 10000, "algorithm": "SHA1", @@ -151,7 +132,7 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { t.Fatalf("method ID is empty") } - secret, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ + secret, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method/totp/admin-generate"), map[string]interface{}{ "entity_id": entityID, "method_id": methodID, }) @@ -269,20 +250,9 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { if secret.Auth == nil || secret.Auth.MFARequirement == nil { t.Fatalf("two phase login returned nil MFARequirement") } - // give it enough time to make sure the request has expired - time.Sleep(605 * time.Second) - _, 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: %v", err) - } // Destroy the secret so that the token can self generate - _, err = userClient.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ + _, err = userClient.Logical().Write(fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ "entity_id": entityID, "method_id": methodID, }) @@ -291,596 +261,3 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { } } } - -//- an enforcement can be defined in any NS and applies to that NS and its children -//- a methodid can be defined in any NS and may be referenced by an enforcement in -// that NS or its children -//- an entity may configure TOTP keys for methods defined in the entity's NS or -// its parents -func TestNamespaceLoginMfaTotp(t *testing.T) { - cluster := vault.NewTestCluster(t, loginMFACoreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - - client := cluster.Cores[0].Client - - // Mount the TOTP backend - mountInfo := &api.MountInput{ - Type: "totp", - } - err := client.Sys().Mount("totp", mountInfo) - if err != nil { - t.Fatalf("failed to mount totp backend: %v", err) - } - - // Mount Userpass for the root namespace - err = client.Sys().EnableAuthWithOptions("userpassRoot", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass auth: %v", err) - } - - // Setup Namespaces and create Userpass mounts in each - // creating namespaces ns1, ns1/ns2, and ns1/ns2/ns3 - - // NS1 - ns1Path := testhelpers.CreateNamespace(t, client, "ns1/", "") - - // Enable Userpass authentication for the NS - client.SetNamespace(ns1Path) - err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass auth: %v", err) - } - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatalf("bb") - } - mountAccessor1 := auths["userpass/"].Accessor - - resp, err := client.Logical().Write("identity/entity", map[string]interface{}{ - "name": "test-entity1", - "metadata": map[string]string{ - "email": "test@hashicorp.com", - "phone_number": "123-456-7890", - }, - }) - if err != nil { - t.Fatalf("failed to creat an entity: %v", err) - } - entityID1 := resp.Data["id"].(string) - - _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ - "name": "testuser1", - "canonical_id": entityID1, - "mount_accessor": getNamespaceSpecificMountAccessor(ns1Path, client, t), - }) - if err != nil { - t.Fatalf("failed to create an entity-alias: %v", err) - } - - // Creating a user in the userpass auth mount - _, err = client.Logical().Write("auth/userpass/users/testuser1", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("failed to configure userpass backend: %v", err) - } - - // NS2 - // Create second namespace ns2 - ns2Path := testhelpers.CreateNamespace(t, client, "ns2/", "ns1/") - client.SetNamespace(ns2Path) - err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass auth: %v", err) - } - auths, err = client.Sys().ListAuth() - if err != nil { - t.Fatalf("bb") - } - mountAccessor2 := auths["userpass/"].Accessor - - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{ - "name": "test-entity2", - "metadata": map[string]string{ - "email": "test@hashicorp.com", - "phone_number": "123-456-7890", - }, - }) - if err != nil { - t.Fatalf("failed to creat an entity: %v", err) - } - entityID2 := resp.Data["id"].(string) - - _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ - "name": "testuser2", - "canonical_id": entityID2, - "mount_accessor": getNamespaceSpecificMountAccessor(ns2Path, client, t), - }) - if err != nil { - t.Fatalf("failed to create an entity-alias: %v", err) - } - // Creating a user in the userpass auth mount - _, err = client.Logical().Write("auth/userpass/users/testuser2", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("failed to configure userpass backend: %v", err) - } - - // NS3 - // Create third namespace within ns1 - ns3Path := testhelpers.CreateNamespace(t, client, "ns3/", "ns1/ns2/") - client.SetNamespace(ns3Path) - err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass auth: %v", err) - } - auths, err = client.Sys().ListAuth() - if err != nil { - t.Fatalf("bb") - } - mountAccessor3 := auths["userpass/"].Accessor - // mountAccessors := []string{mountAccessor1, mountAccessor2, mountAccessor3} - - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{ - "name": "test-entity3", - "metadata": map[string]string{ - "email": "test@hashicorp.com", - "phone_number": "123-456-7890", - }, - }) - if err != nil { - t.Fatalf("failed to creat an entity: %v", err) - } - entityID3 := resp.Data["id"].(string) - - _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ - "name": "testuser3", - "canonical_id": entityID3, - "mount_accessor": getNamespaceSpecificMountAccessor(ns3Path, client, t), - }) - if err != nil { - t.Fatalf("failed to create an entity-alias: %v", err) - } - // Creating a user in the userpass auth mount - _, err = client.Logical().Write("auth/userpass/users/testuser3", map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("failed to configure userpass backend: %v", err) - } - - namespaceEntityIDs := []string{entityID1, entityID2, entityID3} - - // Creating a group for all entities in all namespaces - client.SetNamespace("") - // Create a group - resp, err = client.Logical().Write("identity/group", map[string]interface{}{ - "name": "engineering", - "member_entity_ids": []string{entityID1, entityID2, entityID3}, - }) - if err != nil { - t.Fatalf("failed to create a group: %v", err) - } - - // groupID := resp.Data["id"].(string) - - namespacePaths := []string{ns1Path, ns2Path, ns3Path} - var namespaceMethodIDs []string - for _, nsPath := range namespacePaths { - // MFA TOTP Method for various NS - client.SetNamespace(nsPath) - // create a config - resp, err = client.Logical().Write("identity/mfa/method-id/totp", map[string]interface{}{ - "issuer": "yCorp", - "period": 10000, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": 10, - "qr_size": 100, - }) - - if err != nil || (resp == nil) { - t.Fatalf("bad: resp: %#v\n err: %v", resp, err) - } - - methodID := resp.Data["method_id"].(string) - if methodID == "" { - t.Fatalf("method ID is empty") - } - namespaceMethodIDs = append(namespaceMethodIDs, methodID) - } - - methodIDTotpCodeNameMap := checkGenerateTotp(client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs, t) - - // Creating Login enforcement in NS1 - client.SetNamespace(ns1Path) - // creating MFAEnforcementConfig - _, err = client.Logical().Write("identity/mfa/login-enforcement/LE11", map[string]interface{}{ - "auth_method_accessors": []string{mountAccessor1}, - "identity_entity_ids": []string{entityID1}, - "name": "LE11", - "mfa_method_ids": []string{namespaceMethodIDs[0]}, - }) - if err != nil { - t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) - } - - // creating MFAEnforcementConfig - client.SetNamespace(ns2Path) - _, err = client.Logical().Write("identity/mfa/login-enforcement/LE21", map[string]interface{}{ - "auth_method_accessors": []string{mountAccessor2}, - "identity_entity_ids": []string{entityID2}, - "name": "LE21", - "mfa_method_ids": []string{namespaceMethodIDs[0]}, - }) - if err != nil { - t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) - } - - _, err = client.Logical().Write("identity/mfa/login-enforcement/LE22", map[string]interface{}{ - "auth_method_accessors": []string{mountAccessor2}, - "identity_entity_ids": []string{entityID2}, - "name": "LE22", - "mfa_method_ids": []string{namespaceMethodIDs[1]}, - }) - if err != nil { - t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) - } - - client.SetNamespace(ns3Path) - // creating MFAEnforcementConfig - _, err = client.Logical().Write("identity/mfa/login-enforcement/LE33", map[string]interface{}{ - "auth_method_accessors": []string{mountAccessor3}, - "identity_entity_ids": []string{entityID3}, - "name": "LE33", - "mfa_method_ids": []string{namespaceMethodIDs[2]}, - }) - if err != nil { - t.Fatalf("failed to configure MFAEnforcementConfig: %v", err) - } - - singlePhaseLogin(client, t, "testuser1", entityID1, methodIDTotpCodeNameMap) - singlePhaseLogin(client, t, "testuser2", entityID2, methodIDTotpCodeNameMap) - singlePhaseLogin(client, t, "testuser3", entityID3, methodIDTotpCodeNameMap) - twoPhaseLogin(client, t, "testuser1", entityID1, methodIDTotpCodeNameMap) - twoPhaseLogin(client, t, "testuser2", entityID2, methodIDTotpCodeNameMap) - twoPhaseLogin(client, t, "testuser3", entityID3, methodIDTotpCodeNameMap) - twoPhaseLoginDifferentNamespace(client, t, "testuser3", entityID3, methodIDTotpCodeNameMap) - - checkDestroyTotp(client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs, t) -} - -func singlePhaseLogin(client *api.Client, t *testing.T, username, entityID string, totpCodeMap map[string][]*totpCode) { - headers := client.Headers() - headers.Del("X-Vault-MFA") - client.SetHeaders(headers) - - // getting the passcode - client.SetNamespace("") - - totpCodeStruct := totpCodeMap[entityID] - for _, codeStruct := range totpCodeStruct { - secret, err := client.Logical().Read(fmt.Sprintf("totp/code/%s", codeStruct.name)) - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - passCode, ok := secret.Data["code"].(string) - if !ok && passCode == "" { - t.Fatalf("failed to generate a totp passcode") - } - - // MFA single-phase login - client.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", codeStruct.methodID, passCode)) - } - - // namespace is the same for the same entityID - client.SetNamespace(totpCodeStruct[0].namespacePath) - secret, err := client.Logical().Write(fmt.Sprintf("auth/userpass/login/%s", username), map[string]interface{}{ - "password": "testpassword", - }) - if err != nil { - t.Fatalf("%s MFA failed: %v", username, err) - } - - userpassToken := secret.Auth.ClientToken - - secret, err = client.Logical().Write("auth/token/lookup", map[string]interface{}{ - "token": userpassToken, - }) - if err != nil { - t.Fatalf("failed to lookup userpass authenticated token: %v", err) - } - - entityIDCheck := secret.Data["entity_id"].(string) - if entityIDCheck != entityID { - t.Fatalf("different entityID assigned") - } -} - -func twoPhaseLogin(client *api.Client, t *testing.T, username, entityID string, totpCodeMap map[string][]*totpCode) { - // Two-phase login - headers := client.Headers() - headers.Del("X-Vault-MFA") - client.SetHeaders(headers) - - client.SetNamespace("") - totpCodeStruct := totpCodeMap[entityID] - - methodIDPasscodeMap := make(map[string][]string, 0) - for _, codeStruct := range totpCodeStruct { - secret, err := client.Logical().Read(fmt.Sprintf("totp/code/%s", codeStruct.name)) - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - passCode, ok := secret.Data["code"].(string) - if !ok && passCode == "" { - t.Fatalf("failed to generate a totp passcode") - } - methodIDPasscodeMap[codeStruct.methodID] = []string{passCode} - } - - // namespace is the same for the same entityID - client.SetNamespace(totpCodeStruct[0].namespacePath) - - secret, err := client.Logical().Write(fmt.Sprintf("auth/userpass/login/%s", username), 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 for username %s", username) - } - 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") - } - - // validation - secretValidated, err := client.Logical().Write("sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": methodIDPasscodeMap, - }) - if err != nil || secretValidated == nil { - t.Fatalf("MFA failed: %v", err) - } - - // validate the same request the second time should fail - secret, err = client.Logical().Write("sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": methodIDPasscodeMap, - }) - if err == nil { - t.Fatalf("MFA validate did not fail as expected") - } - if !strings.Contains(err.Error(), "invalid request ID") { - t.Fatalf("expected error invalid request ID, got %s", err.Error()) - } -} - -func twoPhaseLoginDifferentNamespace(client *api.Client, t *testing.T, username, entityID string, totpCodeMap map[string][]*totpCode) { - // Two-phase login - headers := client.Headers() - headers.Del("X-Vault-MFA") - client.SetHeaders(headers) - - client.SetNamespace("") - totpCodeStruct := totpCodeMap[entityID] - - methodIDPasscodeMap := make(map[string][]string, 0) - for _, codeStruct := range totpCodeStruct { - secret, err := client.Logical().Read(fmt.Sprintf("totp/code/%s", codeStruct.name)) - if err != nil { - t.Fatalf("failed to create totp passcode: %v", err) - } - passCode, ok := secret.Data["code"].(string) - if !ok && passCode == "" { - t.Fatalf("failed to generate a totp passcode") - } - methodIDPasscodeMap[codeStruct.methodID] = []string{passCode} - } - - // namespace is the same for the same entityID - client.SetNamespace(totpCodeStruct[0].namespacePath) - - secret, err := client.Logical().Write(fmt.Sprintf("auth/userpass/login/%s", username), 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 for username %s", username) - } - 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") - } - - // validation in a different namespace - client.SetNamespace("") - _, err = client.Logical().Write("sys/mfa/validate", map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": methodIDPasscodeMap, - }) - if err == nil { - t.Fatalf("expected MFA validate to fail: %v", err) - } - if !strings.Contains(err.Error(), "original request was issued in a different namesapce") { - t.Fatalf("unexpected error returned") - } -} - -func checkGenerateTotp(client *api.Client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs []string, t *testing.T) map[string][]*totpCode { - codeNameStructMap := make(map[string][]*totpCode, 0) - - // generating totp in different namespace than entity namespace should fail - client.SetNamespace("") - secret, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[0], - "method_id": namespaceMethodIDs[1], - }) - if err == nil { - t.Fatalf("1failed to generate a TOTP secret on an entity: %v", err) - } - - // non-root namespace - client.SetNamespace(namespacePaths[2]) - secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[2], - "method_id": namespaceMethodIDs[2], - }) - if err != nil { - t.Fatalf("2failed to generate a TOTP secret on an entity: %v", err) - } - totpURL := secret.Data["url"].(string) - - name := namespaceMethodIDs[2] + namespaceEntityIDs[2] - registerTotpUrl(client, totpURL, name, t) - codeNameStructMap[namespaceEntityIDs[2]] = append(codeNameStructMap[namespaceEntityIDs[2]], &totpCode{ - name: name, - methodID: namespaceMethodIDs[2], - namespacePath: namespacePaths[2], - entityID: namespaceEntityIDs[2], - }) - - client.SetNamespace(namespacePaths[0]) - secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[0], - "method_id": namespaceMethodIDs[0], - }) - if err != nil { - t.Fatalf("3failed to generate a TOTP secret on an entity: %v", err) - } - totpURL = secret.Data["url"].(string) - - name = namespaceMethodIDs[0] + namespaceEntityIDs[0] - registerTotpUrl(client, totpURL, name, t) - codeNameStructMap[namespaceEntityIDs[0]] = append(codeNameStructMap[namespaceEntityIDs[0]], &totpCode{ - name: name, - methodID: namespaceMethodIDs[0], - namespacePath: namespacePaths[0], - entityID: namespaceEntityIDs[0], - }) - - client.SetNamespace(namespacePaths[1]) - secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[1], - "method_id": namespaceMethodIDs[0], - }) - if err != nil { - t.Fatalf("failed to generate a TOTP secret on an entity: %v", err) - } - totpURL = secret.Data["url"].(string) - - name = namespaceMethodIDs[0] + namespaceEntityIDs[1] - registerTotpUrl(client, totpURL, name, t) - codeNameStructMap[namespaceEntityIDs[1]] = append(codeNameStructMap[namespaceEntityIDs[1]], &totpCode{ - name: name, - methodID: namespaceMethodIDs[0], - namespacePath: namespacePaths[1], - entityID: namespaceEntityIDs[1], - }) - - client.SetNamespace(namespacePaths[1]) - secret, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-generate"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[1], - "method_id": namespaceMethodIDs[1], - }) - if err != nil { - t.Fatalf("5failed to generate a TOTP secret on an entity: %v", err) - } - totpURL = secret.Data["url"].(string) - - name = namespaceMethodIDs[1] + namespaceEntityIDs[1] - registerTotpUrl(client, totpURL, name, t) - codeNameStructMap[namespaceEntityIDs[1]] = append(codeNameStructMap[namespaceEntityIDs[1]], &totpCode{ - name: name, - methodID: namespaceMethodIDs[1], - namespacePath: namespacePaths[1], - entityID: namespaceEntityIDs[1], - }) - - return codeNameStructMap -} - -func registerTotpUrl(client *api.Client, totpURL, codeName string, t *testing.T) { - client.SetNamespace("") - _, err := client.Logical().Write(fmt.Sprintf("totp/keys/%s", codeName), map[string]interface{}{ - "url": totpURL, - }) - if err != nil { - t.Fatalf("failed to register a TOTP URL: %v", err) - } -} - -func checkDestroyTotp(client *api.Client, namespacePaths, namespaceMethodIDs, namespaceEntityIDs []string, t *testing.T) { - client.SetNamespace("") - - _, err := client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[0], - "method_id": namespaceMethodIDs[1], - }) - if err == nil { - t.Fatalf("failed to destroy the MFA secret: %s", err) - } - - // non-root namespace - client.SetNamespace(namespacePaths[2]) - _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[2], - "method_id": namespaceMethodIDs[2], - }) - if err != nil { - t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) - } - - client.SetNamespace(namespacePaths[0]) - _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[0], - "method_id": namespaceMethodIDs[0], - }) - if err != nil { - t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) - } - - client.SetNamespace(namespacePaths[1]) - _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[1], - "method_id": namespaceMethodIDs[0], - }) - if err != nil { - t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) - } - - client.SetNamespace(namespacePaths[1]) - _, err = client.Logical().Write(fmt.Sprintf("identity/mfa/method-id/totp/admin-destroy"), map[string]interface{}{ - "entity_id": namespaceEntityIDs[1], - "method_id": namespaceMethodIDs[1], - }) - if err != nil { - t.Fatalf("failed to destroy a TOTP secret on an entity: %v", err) - } -} diff --git a/vault/external_tests/mfa/login_mfa_test.go b/vault/external_tests/mfa/login_mfa_test.go index 8370a137c193..fc19c8b8bc17 100644 --- a/vault/external_tests/mfa/login_mfa_test.go +++ b/vault/external_tests/mfa/login_mfa_test.go @@ -13,110 +13,6 @@ import ( "github.com/hashicorp/vault/vault" ) -// TestLoginMFA_LoginEnforcement_UniqueNames tests to ensure that 2 different login enforcements can be created with -// the same name, as long as they're in separate namespaces. -func TestLoginMFA_LoginEnforcement_UniqueNames(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - client := cluster.Cores[0].Client - - // create a few namespaces - _, err := client.Logical().Write("sys/namespaces/foo", nil) - if err != nil { - t.Fatal(err) - } - _, err = client.Logical().Write("sys/namespaces/bar", nil) - if err != nil { - t.Fatal(err) - } - - // create some prereq data - resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "fooCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooConfigId := resp.Data["method_id"].(string) - - resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "barCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - barConfigId := resp.Data["method_id"].(string) - - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) - if err != nil { - t.Fatal(err) - } - bobId := resp.Data["id"].(string) - - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) - if err != nil { - t.Fatal(err) - } - aliceId := resp.Data["id"].(string) - - myPath := "identity/mfa/login-enforcement/baz" - - // create a login enforcement config in the foo ns - client.SetNamespace("foo") - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{fooConfigId}, - "identity_entity_ids": []string{bobId}, - }) - if err != nil { - t.Fatal(err) - } - - // create the same login enforcement config with the same name in the bar ns. - // this should succeed because enforcement config names are unique per ns, - // not globally. - client.SetNamespace("bar") - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{barConfigId}, - "identity_entity_ids": []string{aliceId}, - }) - if err != nil { - t.Fatal(err) - } - - // when we read the foo login enforcement config back out, it should have fooCorp and bob, not barCorp and alice - // because both baz login enforcements were stored separately, since they were in separate namespaces. if they - // weren't stored separately, the second write would've overwritten the first. - client.SetNamespace("foo") - resp, err = client.Logical().Read(myPath) - if err != nil { - t.Fatal(err) - } - if ieid := resp.Data["identity_entity_ids"].([]interface{})[0]; ieid != bobId { - t.Fatalf("expected bob but got %q", ieid) - } - if mmid := resp.Data["mfa_method_ids"].([]interface{})[0]; mmid != fooConfigId { - t.Fatalf("expected %q but got %q", fooConfigId, mmid) - } -} - // TestLoginMFA_Method_CRUD tests creating/reading/updating/deleting a method config for all of the MFA providers func TestLoginMFA_Method_CRUD(t *testing.T) { cluster := vault.NewTestCluster(t, &vault.CoreConfig{ @@ -500,848 +396,6 @@ func TestLoginMFA_LoginEnforcement_RequiredParameters(t *testing.T) { } } -// TestLoginMFA_Method_Namespaces tests to ensure that namespace rules are followed when operating on method configs -func TestLoginMFA_Method_Namespaces(t *testing.T) { - cluster := vault.NewTestCluster(t, &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - }, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - client := cluster.Cores[0].Client - - // Enable userpass authentication - err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatalf("failed to enable userpass auth: %v", err) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - mountAccessor := auths["userpass/"].Accessor - - // create a few namespaces - foo, foo/bar, foo/bar/baz, quux - _, err = client.Logical().Write("sys/namespaces/foo", nil) - if err != nil { - t.Fatal(err) - } - _, err = client.Logical().Write("sys/namespaces/quux", nil) - if err != nil { - t.Fatal(err) - } - client.SetNamespace("foo") - _, err = client.Logical().Write("sys/namespaces/bar", nil) - if err != nil { - t.Fatal(err) - } - client.SetNamespace("foo/bar") - _, err = client.Logical().Write("sys/namespaces/baz", nil) - if err != nil { - t.Fatal(err) - } - - // create a method config in ns foo/bar - resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "yCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooBarMethodId := resp.Data["method_id"].(string) - fooBarPath := fmt.Sprintf("identity/mfa/method/totp/%s", fooBarMethodId) - - // create 2 additional method configs in ns foo - client.SetNamespace("foo") - resp, err = client.Logical().Write("identity/mfa/method/duo", map[string]interface{}{ - "mount_accessor": mountAccessor, - "secret_key": "oIiQkWhGZw3r5gV1cRSUQ9dwiUv4atW4vdTCx2v9", - "integration_key": "DI6XBJ2S2VEDGW8KZ2BH", - "api_hostname": "api-52ae179c.duosecurity.com", - }) - if err != nil { - t.Fatal(err) - } - - resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "aCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooMethodId := resp.Data["method_id"].(string) - - // create another method config in the root ns - client.ClearNamespace() - resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "zCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - rootMethodId := resp.Data["method_id"].(string) - rootPath := fmt.Sprintf("identity/mfa/method/totp/%s", rootMethodId) - - successCallback := func(r *api.Secret, e error) { - if e != nil { - t.Fatal(e) - } - if r != nil && r.Data["error"] != nil { - t.Fatal(r.Data["error"]) - } - } - - failureCallback := func(r *api.Secret, e error) { - if e == nil { - t.Fatal("expected to get an error but didn't get one") - } - } - - testCases := []struct { - name string - action string - namespace string - path string - data map[string]interface{} - callback func(*api.Secret, error) - }{ - { - "read foo/bar from foo/bar", - "read", - "foo/bar", - fooBarPath, - nil, - successCallback, - }, - { - "update foo/bar from foo/bar", - "update", - "foo/bar", - fooBarPath, - map[string]interface{}{"issuer": "lolCorp"}, - successCallback, - }, - { - "read foo/bar from root", - "read", - "", - fooBarPath, - nil, - successCallback, - }, - { - "update foo/bar from root", - "update", - "", - fooBarPath, - map[string]interface{}{"issuer": "lolCorp"}, - failureCallback, - }, - { - "read foo/bar from quux", - "read", - "quux", - fooBarPath, - nil, - failureCallback, - }, - { - "update foo/bar from quux", - "update", - "quux", - fooBarPath, - map[string]interface{}{"issuer": "lolCorp"}, - failureCallback, - }, - { - "read foo/bar from foo/bar/baz", - "read", - "foo/bar/baz", - fooBarPath, - nil, - successCallback, - }, - { - "update foo/bar from foo/bar/baz", - "update", - "foo/bar/baz", - fooBarPath, - map[string]interface{}{"issuer": "lolCorp"}, - failureCallback, - }, - { - "read foo/bar from foo", - "read", - "foo", - fooBarPath, - nil, - successCallback, - }, - { - "update foo/bar from foo", - "update", - "foo", - fooBarPath, - map[string]interface{}{"issuer": "lolCorp"}, - failureCallback, - }, - { - "read root from root", - "read", - "", - rootPath, - nil, - successCallback, - }, - { - "update root from root", - "update", - "", - rootPath, - map[string]interface{}{"issuer": "lolCorp"}, - successCallback, - }, - { - "read root from foo", - "read", - "foo", - rootPath, - nil, - successCallback, - }, - { - "update root from foo", - "update", - "foo", - rootPath, - map[string]interface{}{"issuer": "lolCorp"}, - failureCallback, - }, - { - "list foo/bar from foo/bar", - "list", - "foo/bar", - "identity/mfa/method/totp", - nil, - func(s *api.Secret, e error) { - if e != nil { - t.Fatal(e) - } - if s != nil && s.Data["error"] != nil { - t.Fatal(s.Data["error"]) - } - - // we should get 3 results back when listing foo/bar from foo/bar: - // one from foo/bar itself, one from foo, and one from root. - // note that there are 2 method configs defined in foo, 1 in foo/bar, 1 in root, so 4 total, - // but foo has one totp and one duo. we're listing totp here, so we should not get - // the duo one back. - if k := len(s.Data["keys"].([]interface{})); k != 3 { - t.Fatalf("expected 3 keys but got %d", k) - } - expectedKeys := []string{fooBarMethodId, fooMethodId, rootMethodId} - actualKeys := stringSliceFromInterfaceSlice(s.Data["keys"].([]interface{})) - - if !strutil.EquivalentSlices(actualKeys, expectedKeys) { - t.Fatalf("expected %v to be equivalent to %v but it wasn't", actualKeys, expectedKeys) - } - }, - }, - } - - for _, testCase := range testCases { - name := fmt.Sprintf("%s %s", testCase.action, testCase.name) - t.Run(name, func(t *testing.T) { - if testCase.namespace == "" { - client.ClearNamespace() - } else { - client.SetNamespace(testCase.namespace) - } - - var err error - var resp *api.Secret - - switch testCase.action { - case "read": - resp, err = client.Logical().Read(testCase.path) - case "update": - resp, err = client.Logical().Write(testCase.path, testCase.data) - case "list": - resp, err = client.Logical().List(testCase.path) - } - - testCase.callback(resp, err) - }) - } -} - -func TestLoginMFA_LoginEnforcement_Namespaces(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - client := cluster.Cores[0].Client - - // create a few namespaces - foo, foo/bar, foo/bar/baz, quux - _, err := client.Logical().Write("sys/namespaces/foo", nil) - if err != nil { - t.Fatal(err) - } - _, err = client.Logical().Write("sys/namespaces/quux", nil) - if err != nil { - t.Fatal(err) - } - client.SetNamespace("foo") - _, err = client.Logical().Write("sys/namespaces/bar", nil) - if err != nil { - t.Fatal(err) - } - client.SetNamespace("foo/bar") - _, err = client.Logical().Write("sys/namespaces/baz", nil) - if err != nil { - t.Fatal(err) - } - - // create a method config in ns foo/bar - resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "yCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooBarMethodId := resp.Data["method_id"].(string) - - // create an entity in ns foo/bar - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) - if err != nil { - t.Fatal(err) - } - aliceId := resp.Data["id"].(string) - - // create a login enforcement config in ns foo/bar - data := map[string]interface{}{ - "mfa_method_ids": []string{fooBarMethodId}, - "identity_entity_ids": []string{aliceId}, - } - - fooBarPath := "identity/mfa/login-enforcement/lol" - resp, err = client.Logical().Write(fooBarPath, data) - if err != nil { - t.Fatal(err) - } - - // create a method config in the root ns - client.ClearNamespace() - resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "zCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - rootMethodId := resp.Data["method_id"].(string) - - // create an entity in the root ns - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) - if err != nil { - t.Fatal(err) - } - bobId := resp.Data["id"].(string) - - // create another login enforcement config in the root ns - rootPath := "identity/mfa/login-enforcement/lawl" - data = map[string]interface{}{ - "mfa_method_ids": []string{rootMethodId}, - "identity_entity_ids": []string{bobId}, - } - resp, err = client.Logical().Write(rootPath, data) - if err != nil { - t.Fatal(err) - } - - testCases := []struct { - name string - action string - namespace string - path string - succeed bool - data map[string]interface{} - }{ - { - "read foo/bar from foo/bar", - "read", - "foo/bar", - fooBarPath, - true, - nil, - }, - { - "update foo/bar from foo/bar", - "update", - "foo/bar", - fooBarPath, - true, - map[string]interface{}{ - "mfa_method_ids": []string{fooBarMethodId}, - "identity_entity_ids": []string{aliceId}, - }, - }, - { - "read foo/bar from root", - "read", - "", - fooBarPath, - true, - nil, - }, - { - "update foo/bar from root", - "update", - "", - fooBarPath, - true, - map[string]interface{}{ - "mfa_method_ids": []string{rootMethodId}, - "identity_entity_ids": []string{bobId}, - }, - }, - { - "read foo/bar from quux", - "read", - "quux", - fooBarPath, - false, - nil, - }, - { - "update foo/bar from quux", - "update", - "quux", - fooBarPath, - false, - map[string]interface{}{ - "mfa_method_ids": []string{fooBarMethodId}, - "identity_entity_ids": []string{aliceId}, - }, - }, - { - "read foo/bar from foo/bar/baz", - "read", - "foo/bar/baz", - fooBarPath, - false, - nil, - }, - { - "update foo/bar from foo/bar/baz", - "update", - "foo/bar/baz", - fooBarPath, - false, - map[string]interface{}{ - "mfa_method_ids": []string{fooBarMethodId}, - "identity_entity_ids": []string{aliceId}, - }, - }, - { - "read foo/bar from foo", - "read", - "foo", - fooBarPath, - true, - nil, - }, - { - "update foo/bar from foo", - "update", - "foo", - fooBarPath, - false, - map[string]interface{}{ - "mfa_method_ids": []string{fooBarMethodId}, - "identity_entity_ids": []string{aliceId}, - }, - }, - { - "read root from root", - "read", - "", - rootPath, - true, - nil, - }, - { - "update root from root", - "update", - "", - rootPath, - true, - map[string]interface{}{ - "mfa_method_ids": []string{rootMethodId}, - "identity_entity_ids": []string{bobId}, - }, - }, - { - "read root from foo", - "read", - "foo", - rootPath, - false, - nil, - }, - { - "update root from foo", - "update", - "foo", - rootPath, - false, - map[string]interface{}{ - "mfa_method_ids": []string{rootMethodId}, - "identity_entity_ids": []string{bobId}, - }, - }, - } - - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - if testCase.namespace == "" { - client.ClearNamespace() - } else { - client.SetNamespace(testCase.namespace) - } - - var err error - var resp *api.Secret - - switch testCase.action { - case "read": - resp, err = client.Logical().Read(testCase.path) - case "update": - resp, err = client.Logical().Write(testCase.path, testCase.data) - } - - if testCase.succeed { - if err != nil { - t.Fatal(err) - } - if resp != nil && resp.Data["error"] != nil { - t.Fatal(resp.Data["error"]) - } - } else { - if err == nil && resp != nil { - t.Fatal("expected to get an error but didn't get one") - } - } - }) - } -} - -// TestLoginMFA_LoginEnforcement_ConfigNamespaces tests that a login enforcement config should be able to access method -// ids configured in its own namespace or any of its ancestor namespaces. -func TestLoginMFA_LoginEnforcement_ConfigNamespaces(t *testing.T) { - cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - client := cluster.Cores[0].Client - - // create a few namespaces - foo, foo/bar - _, err := client.Logical().Write("sys/namespaces/foo", nil) - if err != nil { - t.Fatal(err) - } - client.SetNamespace("foo") - _, err = client.Logical().Write("sys/namespaces/bar", nil) - if err != nil { - t.Fatal(err) - } - - // create a method config in the root ns - client.ClearNamespace() - resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "rootCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - rootMethodId := resp.Data["method_id"].(string) - - // create a method config in ns foo - client.SetNamespace("foo") - resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "fooCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooMethodId := resp.Data["method_id"].(string) - - // create a method config in ns foo/bar - client.SetNamespace("foo/bar") - resp, err = client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "fooBarCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooBarMethodId := resp.Data["method_id"].(string) - - // create an entity in ns foo/bar - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) - if err != nil { - t.Fatal(err) - } - bobId := resp.Data["id"].(string) - - // from the foo/bar ns, login enforcement configs should be able to reference any of the method configs - // that were created, since they're all either in foo/bar or are an ancestor of foo/bar - for _, id := range []string{rootMethodId, fooMethodId, fooBarMethodId} { - data := map[string]interface{}{ - "mfa_method_ids": []string{id}, - "identity_entity_ids": []string{bobId}, - } - - resp, err = client.Logical().Write("identity/mfa/login-enforcement/lol", data) - if err != nil { - t.Fatal(err) - } - } -} - -// TestLoginMFA_LoginEnforcement_Validation tests that all of the parameters provided to a login enforcement config -// exist within Vault and aren't just random values. -func TestLoginMFA_LoginEnforcement_Validation(t *testing.T) { - cluster := vault.NewTestCluster(t, &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - }, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) - cluster.Start() - defer cluster.Cleanup() - - core := cluster.Cores[0].Core - vault.TestWaitActive(t, core) - client := cluster.Cores[0].Client - - // create a few namespaces - foo, bar - _, err := client.Logical().Write("sys/namespaces/foo", nil) - if err != nil { - t.Fatal(err) - } - _, err = client.Logical().Write("sys/namespaces/bar", nil) - if err != nil { - t.Fatal(err) - } - - // create a config in ns foo - client.SetNamespace("foo") - resp, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ - "issuer": "fooCorp", - "period": 10, - "algorithm": "SHA1", - "digits": 6, - "skew": 1, - "key_size": uint(10), - "qr_size": 100, - }) - if err != nil { - t.Fatal(err) - } - fooConfigId := resp.Data["method_id"].(string) - - // enable userpass auth in ns foo - err = client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{ - Type: "userpass", - }) - if err != nil { - t.Fatal(err) - } - - auths, err := client.Sys().ListAuth() - if err != nil { - t.Fatal(err) - } - - var mountAccessor string - var mountType string - if auths != nil && auths["userpass/"] != nil { - mountAccessor = auths["userpass/"].Accessor - mountType = auths["userpass/"].Type - } - - // create an entity in ns foo - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "alice"}) - if err != nil { - t.Fatal(err) - } - aliceId := resp.Data["id"].(string) - - // create a group in ns foo - resp, err = client.Logical().Write("identity/group", map[string]interface{}{ - "metadata": map[string]interface{}{"rad": true}, - "member_entity_ids": []string{aliceId}, - }) - if err != nil { - t.Fatal(err) - } - radGroupId := resp.Data["id"].(string) - - // create an entity in ns bar - client.SetNamespace("bar") - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "bob"}) - if err != nil { - t.Fatal(err) - } - bobId := resp.Data["id"].(string) - - // create an entity in root ns - client.ClearNamespace() - resp, err = client.Logical().Write("identity/entity", map[string]interface{}{"name": "cynthia"}) - if err != nil { - t.Fatal(err) - } - cynthiaId := resp.Data["id"].(string) - - myPath := "identity/mfa/login-enforcement/lol" - - // try to create a login enforcement config with a non-existant method id - should fail - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{"wrong"}, - "identity_entity_ids": []string{cynthiaId}, - }) - if err == nil { - t.Fatal("expected an error but didn't get one") - } - - // try to create a login enforcement config using a method id from a different namespace that's not an ancestor - // - should fail - client.SetNamespace("bar") - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{fooConfigId}, - "identity_entity_ids": []string{bobId}, - }) - if err == nil { - t.Fatal("expected an error but didn't get one") - } - - // try to create a login enforcement config with a group id for a non-existant group - should fail - client.SetNamespace("foo") - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": [][]string{{fooConfigId}}, - "identity_group_ids": []string{"nope"}, - }) - if err == nil { - t.Fatal("expected an error but didn't get one") - } - - // try to create a login enforcement config with an entity id for a non-existant entity - should fail - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{fooConfigId}, - "identity_entity_ids": []string{"nope"}, - }) - if err == nil { - t.Fatal("expected an error but didn't get one") - } - - // try to create a login enforcement config with a non-existant auth method accessor - should fail - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{fooConfigId}, - "auth_method_accessors": []string{"wrong"}, - }) - if err == nil { - t.Fatal("expected an error but didn't get one") - } - - // try to create a login enforcement config with a non-existant auth method type - should fail - _, err = client.Logical().Write(myPath, map[string]interface{}{ - "mfa_method_ids": []string{fooConfigId}, - "auth_method_types": []string{"wrong"}, - }) - if err == nil { - t.Fatal("expected an error but didn't get one") - } - - // try to create a login enforcement config using a method id in the correct namespace with valid - // data - should succeed - data := map[string]interface{}{ - "mfa_method_ids": []string{fooConfigId}, - "identity_group_ids": []string{radGroupId}, - "identity_entity_ids": []string{aliceId}, - "auth_method_accessors": []string{mountAccessor}, - "auth_method_types": []string{mountType}, - } - _, err = client.Logical().Write(myPath, data) - if err != nil { - t.Fatal(err) - } -} - func TestLoginMFA_UpdateNonExistentConfig(t *testing.T) { cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, diff --git a/vault/identity_store_oss.go b/vault/identity_store_oss.go index 5cf6e1ff2847..bae17ff49385 100644 --- a/vault/identity_store_oss.go +++ b/vault/identity_store_oss.go @@ -8,10 +8,6 @@ import ( "github.com/hashicorp/vault/helper/identity" ) -func (c *Core) PersistTOTPKey(context.Context, string, string, string) error { - return nil -} - func (c *Core) SendGroupUpdate(context.Context, *identity.Group) (bool, error) { return false, nil } diff --git a/vault/logical_system.go b/vault/logical_system.go index bdba6d489bcf..b04ff71cbd18 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -68,6 +68,10 @@ func systemBackendMemDBSchema() *memdb.DBSchema { return systemSchema } +type PolicyMFABackend struct { + *MFABackend +} + func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { db, _ := memdb.NewMemDB(systemBackendMemDBSchema()) @@ -78,8 +82,6 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { mfaBackend: NewPolicyMFABackend(core, logger), } - core.AddLogger(b.mfaBackend.mfaLogger) - b.Backend = &framework.Backend{ Help: strings.TrimSpace(sysHelpRoot), @@ -185,6 +187,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend { b.Backend.Paths = append(b.Backend.Paths, b.hostInfoPath()) b.Backend.Paths = append(b.Backend.Paths, b.quotasPaths()...) b.Backend.Paths = append(b.Backend.Paths, b.rootActivityPaths()...) + b.Backend.Paths = append(b.Backend.Paths, b.loginMFAPaths()...) if core.rawEnabled { b.Backend.Paths = append(b.Backend.Paths, b.rawPaths()...) diff --git a/vault/login_mfa.go b/vault/login_mfa.go index 017024860936..0cd513f0eb89 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -10,7 +10,6 @@ import ( "io/ioutil" "net/http" "net/url" - "path" "strings" "sync" "time" @@ -26,13 +25,11 @@ import ( "github.com/hashicorp/go-memdb" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault-licensing/license" "github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/identity/mfa" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/jsonutil" - osslicense "github.com/hashicorp/vault/sdk/helper/license" "github.com/hashicorp/vault/sdk/helper/parseutil" "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/logical" @@ -63,7 +60,7 @@ type totpKey struct { // loginMfaPaths returns the API endpoints to configure the new style // login MFA. The following paths are supported: -// mfa/method-id/:mfa_method - management of MFA method IDs, which can be used for configuration +// mfa/method/:mfa_method - management of MFA method IDs, which can be used for configuration // mfa/login_enforcement/:config_name - configures single or two phase MFA auth func (b *SystemBackend) loginMFAPaths() []*framework.Path { return []*framework.Path{ @@ -825,289 +822,6 @@ func (b *LoginMFABackend) validateAuthEntriesForAccessorOrType(ctx context.Conte return false, nil } -func (b *SystemBackend) mfaPaths() []*framework.Path { - return []*framework.Path{ - { - Pattern: "mfa/method/?", - FeatureRequired: osslicense.Features(license.FeatureMFA), - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.mfaBackend.pathMFAMethodsList, - }, - HelpSynopsis: strings.TrimSpace(mfaHelp["methods-list"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["methods-list"][1]), - }, - { - Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name") + "/generate$", - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: "Name of the MFA method.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.mfaBackend.handleMFAGenerateRead, - }, - - HelpSynopsis: strings.TrimSpace(mfaHelp["totp-generate"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["totp-generate"][1]), - }, - { - Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name") + "/admin-generate$", - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: "Name of the MFA method.", - }, - "entity_id": { - Type: framework.TypeString, - Description: "Entity ID on which the generated secret needs to get stored.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.mfaBackend.handleMFAAdminGenerateUpdate, - }, - HelpSynopsis: strings.TrimSpace(mfaHelp["totp-admin-generate"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["totp-admin-generate"][1]), - }, - { - Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name") + "/admin-destroy$", - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: "Name of the MFA method.", - }, - "entity_id": { - Type: framework.TypeString, - Description: "Identifier of the entity from which the MFA method secret needs to be removed.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: b.mfaBackend.handleMFAAdminDestroyUpdate, - }, - HelpSynopsis: strings.TrimSpace(mfaHelp["totp-admin-destroy"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["totp-admin-destroy"][1]), - }, - { - Pattern: "mfa/method/totp/" + framework.GenericNameRegex("name"), - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: `Name of the MFA method.`, - }, - "issuer": { - Type: framework.TypeString, - Description: `The name of the key's issuing organization.`, - }, - "period": { - Type: framework.TypeDurationSecond, - Default: 30, - Description: `The length of time used to generate a counter for the TOTP token calculation.`, - }, - "key_size": { - Type: framework.TypeInt, - Default: 20, - Description: "Determines the size in bytes of the generated key.", - }, - "qr_size": { - Type: framework.TypeInt, - Default: 200, - Description: `The pixel size of the generated square QR code.`, - }, - "algorithm": { - Type: framework.TypeString, - Default: "SHA1", - Description: `The hashing algorithm used to generate the TOTP token. Options include SHA1, SHA256 and SHA512.`, - }, - "digits": { - Type: framework.TypeInt, - Default: 6, - Description: `The number of digits in the generated TOTP token. This value can either be 6 or 8.`, - }, - "skew": { - Type: framework.TypeInt, - Default: 1, - Description: `The number of delay periods that are allowed when validating a TOTP token. This value can either be 0 or 1.`, - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: validateRootNS(b.mfaBackend.handleTOTPConfigUpdate), - logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), - logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), - }, - - HelpSynopsis: strings.TrimSpace(mfaHelp["totp-method"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["totp-method"][1]), - }, - { - Pattern: "mfa/method/okta/" + framework.GenericNameRegex("name"), - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: `Name of the MFA method.`, - }, - "mount_accessor": { - Type: framework.TypeString, - Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, - }, - "username_format": { - Type: framework.TypeString, - Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: - -alias.name: The name returned by the mount configured via the mount_accessor parameter - -If blank, the Alias's name field will be used as-is. -`, - }, - "org_name": { - Type: framework.TypeString, - Description: "Name of the organization to be used in the Okta API.", - }, - "api_token": { - Type: framework.TypeString, - Description: "Okta API key.", - }, - "base_url": { - Type: framework.TypeString, - Description: `The base domain to use for the Okta API. When not specified in the configuration, "okta.com" is used.`, - }, - "primary_email": { - Type: framework.TypeBool, - Description: `If true, the username will only match the primary email for the account. Defaults to false.`, - }, - "production": { - Type: framework.TypeBool, - Description: "(DEPRECATED) Use base_url instead.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: validateRootNS(b.mfaBackend.handleOktaConfigUpdate), - logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), - logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), - }, - - HelpSynopsis: strings.TrimSpace(mfaHelp["okta-method"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["okta-method"][1]), - }, - { - Pattern: "mfa/method/duo/" + framework.GenericNameRegex("name"), - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: `Name of the MFA method.`, - }, - "mount_accessor": { - Type: framework.TypeString, - Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, - }, - "username_format": { - Type: framework.TypeString, - Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: - -alias.name: The name returned by the mount configured via the mount_accessor parameter - -If blank, the Alias's name field will be used as-is. -`, - }, - "secret_key": { - Type: framework.TypeString, - Description: "Secret key for Duo.", - }, - "integration_key": { - Type: framework.TypeString, - Description: "Integration key for Duo.", - }, - "api_hostname": { - Type: framework.TypeString, - Description: "API host name for Duo.", - }, - "push_info": { - Type: framework.TypeString, - Description: "Push information for Duo.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: validateRootNS(b.mfaBackend.handleDuoConfigUpdate), - logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), - logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), - }, - - HelpSynopsis: strings.TrimSpace(mfaHelp["duo-method"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["duo-method"][1]), - }, - { - Pattern: "mfa/method/pingid/" + framework.GenericNameRegex("name"), - FeatureRequired: osslicense.Features(license.FeatureMFA), - - Fields: map[string]*framework.FieldSchema{ - "name": { - Type: framework.TypeString, - Description: `Name of the MFA method.`, - }, - "mount_accessor": { - Type: framework.TypeString, - Description: `The mount to tie this method to for use in automatic mappings. The mapping will use the Name field of Aliases associated with this mount as the username in the mapping.`, - }, - "username_format": { - Type: framework.TypeString, - Description: `A format string for mapping Identity names to MFA method names. Values to subtitute should be placed in {{}}. For example, "{{alias.name}}@example.com". Currently-supported mappings: - -alias.name: The name returned by the mount configured via the mount_accessor parameter - -If blank, the Alias's name field will be used as-is. -`, - }, - "settings_file_base64": { - Type: framework.TypeString, - Description: "The settings file provided by Ping, Base64-encoded. This must be a settings file suitable for third-party clients, not the PingID SDK or PingFederate.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.UpdateOperation: validateRootNS(b.mfaBackend.handlePingIDConfigUpdate), - logical.DeleteOperation: validateRootNS(b.mfaBackend.handleMFAConfigDelete), - logical.ReadOperation: validateRootNS(b.mfaBackend.handleMFAConfigRead), - }, - - HelpSynopsis: strings.TrimSpace(mfaHelp["pingid-method"][0]), - HelpDescription: strings.TrimSpace(mfaHelp["pingid-method"][1]), - }, - } -} - -func validateRootNS(f framework.OperationFunc) framework.OperationFunc { - return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - ns, err := namespace.FromContext(ctx) - if err != nil { - return nil, err - } - if ns == nil { - return nil, namespace.ErrNoNamespace - } - if ns.ID != namespace.RootNamespaceID { - return logical.ErrorResponse("this API path can only be called from the root namespace"), nil - } - return f(ctx, req, d) - } -} - func (c *Core) PersistTOTPKey(ctx context.Context, methodID, entityID, key string) error { ks := &totpKey{ Key: key, @@ -2279,28 +1993,6 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec return nil } -func mfaConfigTableSchema() *memdb.TableSchema { - return &memdb.TableSchema{ - Name: memDBMFAConfigsTable, - Indexes: map[string]*memdb.IndexSchema{ - "id": { - Name: "id", - Unique: true, - Indexer: &memdb.StringFieldIndex{ - Field: "ID", - }, - }, - "name": { - Name: "name", - Unique: true, - Indexer: &memdb.StringFieldIndex{ - Field: "Name", - }, - }, - }, - } -} - func loginMFAConfigTableSchema() *memdb.TableSchema { return &memdb.TableSchema{ Name: memDBLoginMFAConfigsTable, @@ -2521,7 +2213,12 @@ func (b *LoginMFABackend) deleteMFALoginEnforcementConfigByNameAndNamespace(ctx } entryIndex := mfaLoginEnforcementPrefix + eConfig.ID - barrierView := b.barrierViewForNamespace(eConfig.NamespaceID) + + barrierView, err := b.Core.barrierViewForNamespace(eConfig.NamespaceID) + if err != nil { + return err + } + err = barrierView.Delete(ctx, entryIndex) if err != nil { return err @@ -2675,7 +2372,10 @@ func (b *LoginMFABackend) MemDBDeleteMFAConfigByIDInTxn(txn *memdb.Txn, configID } func (b *LoginMFABackend) putMFAConfigByID(ctx context.Context, mConfig *mfa.Config) error { - barrierView := b.barrierViewForNamespace(mConfig.NamespaceID) + barrierView, err := b.Core.barrierViewForNamespace(mConfig.NamespaceID) + if err != nil { + return err + } return b.putMFAConfigCommon(ctx, mConfig, loginMFAConfigPrefix, mConfig.ID, barrierView) } @@ -2718,7 +2418,11 @@ func (b *LoginMFABackend) putMFALoginEnforcementConfig(ctx context.Context, eCon return err } - barrierView := b.barrierViewForNamespace(eConfig.NamespaceID) + barrierView, err := b.Core.barrierViewForNamespace(eConfig.NamespaceID) + if err != nil { + return err + } + return barrierView.Put(ctx, &logical.StorageEntry{ Key: entryIndex, Value: marshaledEntry, @@ -2726,7 +2430,10 @@ func (b *LoginMFABackend) putMFALoginEnforcementConfig(ctx context.Context, eCon } func (b *LoginMFABackend) getMFALoginEnforcementConfig(ctx context.Context, key, namespaceId string) (*mfa.MFAEnforcementConfig, error) { - barrierView := b.barrierViewForNamespace(namespaceId) + barrierView, err := b.Core.barrierViewForNamespace(namespaceId) + if err != nil { + return nil, err + } entry, err := barrierView.Get(ctx, mfaLoginEnforcementPrefix+key) if err != nil { return nil, err @@ -2744,17 +2451,6 @@ func (b *LoginMFABackend) getMFALoginEnforcementConfig(ctx context.Context, key, return &eConfig, nil } -func (b *LoginMFABackend) barrierViewForNamespace(namespaceId string) *BarrierView { - var barrierView *BarrierView - if namespaceId == namespace.RootNamespaceID { - barrierView = b.Core.systemBarrierView - } else { - barrierView = b.Core.nsView.SubView(path.Join(namespaceId, systemBarrierPrefix) + "/") - } - - return barrierView -} - var mfaHelp = map[string][2]string{ "methods-list": { "Lists all the available MFA methods by their name.", diff --git a/vault/request_handling.go b/vault/request_handling.go index 06203d2d3e9a..41c46d799e1b 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -1473,7 +1473,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re RequestNSPath: ns.Path, RequestConnRemoteAddr: req.Connection.RemoteAddr, // this is needed for the DUO method TimeOfStorage: time.Now(), - RequestID: req.ID, + RequestID: mfaRequestID, } err = c.SaveMFAResponseAuth(respAuth) if err != nil { From a0a3dc0a4edf3f89a5efa95ec24474e1ca36e686 Mon Sep 17 00:00:00 2001 From: hghaf099 <83242695+hghaf099@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:38:57 -0500 Subject: [PATCH 03/17] Delete method id if not used in an MFA enforcement config (#14063) * Delete an MFA methodID only if it is not used by an MFA enforcement config * Fixing a bug: mfa/validate is an unauthenticated path, and goes through the handleLoginRequest path --- vault/login_mfa.go | 12 ++++++++++++ vault/request_handling.go | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/vault/login_mfa.go b/vault/login_mfa.go index 0cd513f0eb89..e8f05240ef16 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -2272,6 +2272,18 @@ func (b *LoginMFABackend) deleteMFAConfigByMethodID(ctx context.Context, configI b.mfaLock.Lock() defer b.mfaLock.Unlock() + eConfigIter, err := b.MemDBMFALoginEnforcementConfigIterator() + if err != nil { + return err + } + + for eConfigRaw := eConfigIter.Next(); eConfigRaw != nil; eConfigRaw = eConfigIter.Next() { + eConfig := eConfigRaw.(*mfa.MFAEnforcementConfig) + if strutil.StrListContains(eConfig.MFAMethodIDs, configID) { + return fmt.Errorf("methodID is still used by an enforcement configuration with ID: %s", eConfig.ID) + } + } + // Delete the config from storage entryIndex := prefix + configID err = b.Core.systemBarrierView.Delete(ctx, entryIndex) diff --git a/vault/request_handling.go b/vault/request_handling.go index 41c46d799e1b..f3c4c3992de5 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -1075,7 +1075,7 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp // Only the token store is allowed to return an auth block, for any // other request this is an internal error. if resp != nil && resp.Auth != nil { - if !strings.HasPrefix(req.Path, "auth/token/") && req.Path != "sys/mfa/validate" { + if !strings.HasPrefix(req.Path, "auth/token/") { c.logger.Error("unexpected Auth response for non-token backend", "request_path", req.Path) retErr = multierror.Append(retErr, ErrInternalError) return nil, auth, retErr @@ -1303,7 +1303,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re return } // If the response generated an authentication, then generate the token - if resp != nil && resp.Auth != nil { + if resp != nil && resp.Auth != nil && req.Path != "sys/mfa/validate" { leaseGenerated := false // by placing this after the authorization check, we don't leak From db06633d70de118f12e0a819ba3c2e5a7efcf708 Mon Sep 17 00:00:00 2001 From: hghaf099 <83242695+hghaf099@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:45:13 -0500 Subject: [PATCH 04/17] adding use_passcode field to DUO config (#14059) --- helper/identity/mfa/types.pb.go | 155 +++++++++++++++++--------------- helper/identity/mfa/types.proto | 2 + vault/identity_store.go | 4 + vault/login_mfa.go | 2 + vault/request_handling.go | 10 ++- 5 files changed, 100 insertions(+), 73 deletions(-) diff --git a/helper/identity/mfa/types.pb.go b/helper/identity/mfa/types.pb.go index 62f283e79025..0cfa12fa29a5 100644 --- a/helper/identity/mfa/types.pb.go +++ b/helper/identity/mfa/types.pb.go @@ -310,6 +310,8 @@ type DuoConfig struct { APIHostname string `protobuf:"bytes,3,opt,name=api_hostname,json=apiHostname,proto3" json:"api_hostname,omitempty" sentinel:"-"` // @inject_tag: sentinel:"-" PushInfo string `protobuf:"bytes,4,opt,name=push_info,json=pushInfo,proto3" json:"push_info,omitempty" sentinel:"-"` + // @inject_tag: sentinel:"-" + UsePasscode bool `protobuf:"varint,5,opt,name=use_passcode,json=usePasscode,proto3" json:"use_passcode,omitempty" sentinel:"-"` } func (x *DuoConfig) Reset() { @@ -372,6 +374,13 @@ func (x *DuoConfig) GetPushInfo() string { return "" } +func (x *DuoConfig) GetUsePasscode() bool { + if x != nil { + return x.UsePasscode + } + return false +} + // OktaConfig contains Okta configuration parameters required to perform Okta // authentication. type OktaConfig struct { @@ -901,7 +910,7 @@ 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, 0x93, 0x01, 0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, + 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, @@ -910,77 +919,79 @@ var file_helper_identity_mfa_types_proto_rawDesc = []byte{ 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, 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, + 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 914eda6cf70a..c20386cb9011 100644 --- a/helper/identity/mfa/types.proto +++ b/helper/identity/mfa/types.proto @@ -63,6 +63,8 @@ message DuoConfig { string api_hostname = 3; // @inject_tag: sentinel:"-" string push_info = 4; + // @inject_tag: sentinel:"-" + bool use_passcode = 5; } // OktaConfig contains Okta configuration parameters required to perform Okta diff --git a/vault/identity_store.go b/vault/identity_store.go index 9d21b27f8c24..f0b6de7973e0 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -361,6 +361,10 @@ func mfaPaths(i *IdentityStore) []*framework.Path { Type: framework.TypeString, Description: "Push information for Duo.", }, + "use_passcode": { + Type: framework.TypeBool, + Description: `If true, the user is reminded to use the passcode upon MFA validation. This option does not enforce using the passcode. Defaults to false.`, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ diff --git a/vault/login_mfa.go b/vault/login_mfa.go index e8f05240ef16..abc7a4b57e93 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -979,6 +979,7 @@ func parseDuoConfig(mConfig *mfa.Config, d *framework.FieldData) error { IntegrationKey: integrationKey, APIHostname: apiHostname, PushInfo: d.Get("push_info").(string), + UsePasscode: d.Get("use_passcode").(bool), } mConfig.Config = &mfa.Config_DuoConfig{ @@ -1164,6 +1165,7 @@ func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config) (map[string]interface{} respData["pushinfo"] = duoConfig.PushInfo respData["mount_accessor"] = mConfig.MountAccessor respData["username_format"] = mConfig.UsernameFormat + respData["use_passcode"] = duoConfig.UsePasscode case *mfa.Config_PingIDConfig: pingConfig := mConfig.GetPingIDConfig() respData["use_signature"] = pingConfig.UseSignature diff --git a/vault/request_handling.go b/vault/request_handling.go index f3c4c3992de5..7b8eeab4d684 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -1631,10 +1631,18 @@ func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (* if err != nil { return nil, fmt.Errorf("failed to get methodID %s from MFA config table, error: %v", methodID, err) } + var duoUsePasscode bool + if mConfig.Type == mfaMethodTypeDuo { + duoConf, ok := mConfig.Config.(*mfa.Config_DuoConfig) + if !ok { + return nil, fmt.Errorf("invalid MFA configuration type") + } + duoUsePasscode = duoConf.DuoConfig.UsePasscode + } mfaMethod := &logical.MFAMethodID{ Type: mConfig.Type, ID: methodID, - UsesPasscode: mConfig.Type == mfaMethodTypeTOTP || mConfig.Type == mfaMethodTypeDuo, + UsesPasscode: mConfig.Type == mfaMethodTypeTOTP || duoUsePasscode, } mfaAny.Any = append(mfaAny.Any, mfaMethod) } From 0626c75bd80e624c88fb1a2f0a00886dd07f8187 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Wed, 16 Feb 2022 11:26:54 -0800 Subject: [PATCH 05/17] add changelog --- changelog/14025.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/14025.txt diff --git a/changelog/14025.txt b/changelog/14025.txt new file mode 100644 index 000000000000..6e988832fb49 --- /dev/null +++ b/changelog/14025.txt @@ -0,0 +1,3 @@ +```release-note:feature +auth: Add support for single and two phase MFA to login endpoints. +``` From 02c8b5d10fb55099a3a6dada896586bfee4844ac Mon Sep 17 00:00:00 2001 From: hghaf099 <83242695+hghaf099@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:27:58 -0500 Subject: [PATCH 06/17] preventing replay attack on MFA passcodes (#14056) * preventing replay attack on MFA passcodes * using %w instead of %s for error --- .../identity/login_mfa_totp_test.go | 30 +++++++++++++++- vault/login_mfa.go | 34 ++++++++++++++++++- vault/request_handling.go | 1 + 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index ebb17c2d7203..dd7b25628ea5 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -2,7 +2,9 @@ package identity import ( "fmt" + "strings" "testing" + "time" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/builtin/credential/userpass" @@ -115,7 +117,7 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { // create a config resp1, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{ "issuer": "yCorp", - "period": 10000, + "period": 5, "algorithm": "SHA1", "digits": 6, "skew": 1, @@ -206,6 +208,10 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { t.Fatalf("MFA failed: %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") + } + if secret.Auth == nil || secret.Auth.MFARequirement == nil { t.Fatalf("two phase login returned nil MFARequirement") } @@ -229,6 +235,15 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { } // 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) + secret, err = user2Client.Logical().Write("sys/mfa/validate", map[string]interface{}{ "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, "mfa_payload": map[string][]string{ @@ -251,6 +266,19 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { 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, diff --git a/vault/login_mfa.go b/vault/login_mfa.go index abc7a4b57e93..f2c363e6eae6 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -35,6 +35,7 @@ import ( "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault/quotas" "github.com/mitchellh/mapstructure" + "github.com/patrickmn/go-cache" otplib "github.com/pquerna/otp" totplib "github.com/pquerna/otp/totp" ) @@ -99,6 +100,7 @@ type MFABackend struct { mfaLogger hclog.Logger namespacer Namespacer methodTable string + usedCodes *cache.Cache } type LoginMFABackend struct { @@ -138,6 +140,7 @@ func NewMFABackend(core *Core, logger hclog.Logger, prefix string, schemaFuncs [ mfaLogger: logger.Named("mfa"), namespacer: core, methodTable: prefix, + usedCodes: cache.New(0, 30*time.Second), } } @@ -553,7 +556,7 @@ func (b *LoginMFABackend) handleMFALoginValidate(ctx context.Context, req *logic for _, eConfig := range matchedMfaEnforcementList { err = b.Core.validateLoginMFA(ctx, eConfig, entity, req.Connection.RemoteAddr, mfaCreds) if err != nil { - return logical.ErrorResponse("failed to satisfy enforcement %s", eConfig.Name), logical.ErrPermissionDenied + return logical.ErrorResponse(fmt.Sprintf("failed to satisfy enforcement %s. error: %s", eConfig.Name, err.Error())), logical.ErrPermissionDenied } } @@ -1566,9 +1569,16 @@ func (c *Core) validateDuo(ctx context.Context, creds []string, mConfig *mfa.Con return fmt.Errorf("invalid response from Duo preauth: %q", preauth.Response.Result) } + var usedName string options := []func(*url.Values){} factor := "push" if passcode != "" { + usedName = fmt.Sprintf("%s_%s", mConfig.ID, passcode) + _, ok := c.loginMFABackend.usedCodes.Get(usedName) + if ok { + return fmt.Errorf("code already used; wait until the next time period") + } + factor = "passcode" options = append(options, authapi.AuthPasscode(passcode)) } else { @@ -1610,6 +1620,10 @@ func (c *Core) validateDuo(ctx context.Context, creds []string, mConfig *mfa.Con case "deny": return fmt.Errorf("duo authentication failed: %q", statusResult.Response.Status_Msg) case "allow": + err = c.loginMFABackend.usedCodes.Add(usedName, nil, 30*time.Second) + if err != nil { + return fmt.Errorf("error adding code to used cache: %w", err) + } return nil } @@ -1962,6 +1976,13 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec return fmt.Errorf("more than one TOTP passcode supplied") } + usedName := fmt.Sprintf("%s_%s", configID, creds[0]) + + _, ok := c.loginMFABackend.usedCodes.Get(usedName) + if ok { + return fmt.Errorf("code already used; wait until the next time period") + } + totpSecret := entityMethodSecret.GetTOTPSecret() if totpSecret == nil { return fmt.Errorf("entity does not contain the TOTP secret") @@ -1992,6 +2013,17 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec return fmt.Errorf("failed to validate TOTP passcode") } + // Adding the used code to the cache + // Take the key skew, add two for behind and in front, and multiply that by + // the period to cover the full possibility of the validity of the key + err = c.loginMFABackend.usedCodes.Add(usedName, nil, time.Duration( + int64(time.Second)* + int64(totpSecret.Period)* + int64(2+totpSecret.Skew))) + if err != nil { + return fmt.Errorf("error adding code to used cache: %w", err) + } + return nil } diff --git a/vault/request_handling.go b/vault/request_handling.go index 7b8eeab4d684..c22495a2c98f 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -1483,6 +1483,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re resp.Auth = &logical.Auth{ MFARequirement: mfaRequirement, } + resp.AddWarning("A login request was issued that is subject to MFA validation. Please make sure to validate the login by sending another request to mfa/validate endpoint.") // going to return early before generating the token // the user receives the mfaRequirement, and need to use the // login MFA validate endpoint to get the token From d360deecdd0c8c6fc0344a7349eda85c2ecb892d Mon Sep 17 00:00:00 2001 From: hghaf099 <83242695+hghaf099@users.noreply.github.com> Date: Wed, 16 Feb 2022 14:52:08 -0500 Subject: [PATCH 07/17] Improve CLI command for login mfa (#14106) CLI prints a warning message indicating the login request needs to get validated --- command/write.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/command/write.go b/command/write.go index dd0d7cfde99e..0643d8c65314 100644 --- a/command/write.go +++ b/command/write.go @@ -146,6 +146,11 @@ func (c *WriteCommand) Run(args []string) int { return 0 } + if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil { + c.UI.Warn(wrapAtLength("WARNING! A login request was issued that is subject to "+ + "MFA validation. Please make sure to validate the login by sending another "+ + "request to mfa/validate endpoint.") + "\n") + } // Handle single field output if c.flagField != "" { return PrintRawField(c.UI, secret, c.flagField) From 03b50a521ec786f34e30e90a618a004e922253b6 Mon Sep 17 00:00:00 2001 From: hghaf099 <83242695+hghaf099@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:30:52 -0500 Subject: [PATCH 08/17] adding the validity period of a passcode to error messages (#14115) --- vault/login_mfa.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/vault/login_mfa.go b/vault/login_mfa.go index f2c363e6eae6..86f01af08f4c 100644 --- a/vault/login_mfa.go +++ b/vault/login_mfa.go @@ -1576,7 +1576,7 @@ func (c *Core) validateDuo(ctx context.Context, creds []string, mConfig *mfa.Con usedName = fmt.Sprintf("%s_%s", mConfig.ID, passcode) _, ok := c.loginMFABackend.usedCodes.Get(usedName) if ok { - return fmt.Errorf("code already used; wait until the next time period") + return fmt.Errorf("code already used; new code is available in 30 seconds") } factor = "passcode" @@ -1976,16 +1976,20 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec return fmt.Errorf("more than one TOTP passcode supplied") } + totpSecret := entityMethodSecret.GetTOTPSecret() + if totpSecret == nil { + return fmt.Errorf("entity does not contain the TOTP secret") + } + + // Take the key skew, add two for behind and in front, and multiply that by + // the period to cover the full possibility of the validity of the key + validityPeriod := time.Duration(int64(time.Second) * int64(totpSecret.Period) * int64(2+totpSecret.Skew)) + usedName := fmt.Sprintf("%s_%s", configID, creds[0]) _, ok := c.loginMFABackend.usedCodes.Get(usedName) if ok { - return fmt.Errorf("code already used; wait until the next time period") - } - - totpSecret := entityMethodSecret.GetTOTPSecret() - if totpSecret == nil { - return fmt.Errorf("entity does not contain the TOTP secret") + return fmt.Errorf("code already used; new code is available in %v seconds", validityPeriod) } key, err := c.fetchTOTPKey(ctx, configID, entityID) @@ -2014,12 +2018,7 @@ func (c *Core) validateTOTP(ctx context.Context, creds []string, entityMethodSec } // Adding the used code to the cache - // Take the key skew, add two for behind and in front, and multiply that by - // the period to cover the full possibility of the validity of the key - err = c.loginMFABackend.usedCodes.Add(usedName, nil, time.Duration( - int64(time.Second)* - int64(totpSecret.Period)* - int64(2+totpSecret.Skew))) + err = c.loginMFABackend.usedCodes.Add(usedName, nil, validityPeriod) if err != nil { return fmt.Errorf("error adding code to used cache: %w", err) } From e7e8dc183b9bccdbb88116d930d00d93c574fda2 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Thu, 17 Feb 2022 11:20:40 -0800 Subject: [PATCH 09/17] interactive CLI for mfa login --- command/write.go | 85 +++++++++++++++++++ .../identity/login_mfa_totp_test.go | 61 ++++++++++--- 2 files changed, 133 insertions(+), 13 deletions(-) diff --git a/command/write.go b/command/write.go index 0643d8c65314..41e1da61051d 100644 --- a/command/write.go +++ b/command/write.go @@ -1,10 +1,14 @@ package command import ( + "bufio" "fmt" "io" "os" "strings" + "syscall" + + "golang.org/x/term" "github.com/mitchellh/cli" "github.com/posener/complete" @@ -130,6 +134,7 @@ func (c *WriteCommand) Run(args []string) int { return 2 } +WRITE: secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) @@ -150,7 +155,39 @@ func (c *WriteCommand) Run(args []string) int { c.UI.Warn(wrapAtLength("WARNING! A login request was issued that is subject to "+ "MFA validation. Please make sure to validate the login by sending another "+ "request to mfa/validate endpoint.") + "\n") + ok := YesNoPrompt("Would you like to interactively validate MFA?", true) + if ok { + mfaPayload := make(map[string][]string, 0) + for name, mfaConstraintAny := range secret.Auth.MFARequirement.MFAConstraints { + methodIDs := make([]string, 0) + for _, m := range mfaConstraintAny.Any { + methodIDs = append(methodIDs, m.ID) + } + mfaMethodID := StringPrompt(fmt.Sprintf("From MFARequirement %q, please select one of the following methodIDs %q:\n", name, methodIDs)) + if mfaMethodID == "" { + c.UI.Warn("Invalid method ID detected, please validate the login by sending a request to mfa/validate") + goto SKIP + } + passcode, err := PasswordPrompt(fmt.Sprintf("Please insert the passcode for methodID %q: ", mfaMethodID)) + if err != nil { + c.UI.Error(fmt.Sprintf("Failed to read the passcode with error %q. please validate the login by sending a request to mfa/validate.", err.Error())) + goto SKIP + } + // passcode could be an empty string + mfaPayload[mfaMethodID] = []string{passcode} + } + + // updating data and path + data = map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": mfaPayload, + } + path = "sys/mfa/validate" + goto WRITE + } } + +SKIP: // Handle single field output if c.flagField != "" { return PrintRawField(c.UI, secret, c.flagField) @@ -158,3 +195,51 @@ func (c *WriteCommand) Run(args []string) int { return OutputSecret(c.UI, secret) } + +func StringPrompt(label string) string { + var s string + r := bufio.NewReader(os.Stdin) + for { + fmt.Fprint(os.Stderr, label+" ") + s, _ = r.ReadString('\n') + if s != "" { + break + } + } + return strings.TrimSpace(s) +} + +func YesNoPrompt(label string, def bool) bool { + choices := "Y/n" + if !def { + choices = "y/N" + } + + r := bufio.NewReader(os.Stdin) + var s string + + for { + fmt.Fprintf(os.Stderr, "%s (%s) ", label, choices) + s, _ = r.ReadString('\n') + s = strings.TrimSpace(s) + if s == "" { + return def + } + s = strings.ToLower(s) + if s == "y" || s == "yes" { + return true + } + if s == "n" || s == "no" { + return false + } + } +} + +func PasswordPrompt(label string) (string, error) { + fmt.Fprint(os.Stderr, label+" ") + b, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", fmt.Errorf("failed to read the password") + } + return string(b), nil +} diff --git a/vault/external_tests/identity/login_mfa_totp_test.go b/vault/external_tests/identity/login_mfa_totp_test.go index dd7b25628ea5..9600688a2ca9 100644 --- a/vault/external_tests/identity/login_mfa_totp_test.go +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -1,12 +1,14 @@ package identity import ( + "context" "fmt" "strings" "testing" "time" "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/builtin/credential/userpass" "github.com/hashicorp/vault/builtin/logical/totp" vaulthttp "github.com/hashicorp/vault/http" @@ -14,29 +16,44 @@ import ( "github.com/hashicorp/vault/vault" ) -var loginMFACoreConfig = &vault.CoreConfig{ - CredentialBackends: map[string]logical.Factory{ - "userpass": userpass.Factory, - }, - LogicalBackends: map[string]logical.Factory{ - "totp": totp.Factory, +func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) { + var noop *vault.NoopAudit + + cluster := vault.NewTestCluster(t, &vault.CoreConfig{ + CredentialBackends: map[string]logical.Factory{ + "userpass": userpass.Factory, + }, + LogicalBackends: map[string]logical.Factory{ + "totp": totp.Factory, + }, + AuditBackends: map[string]audit.Factory{ + "noop": func(ctx context.Context, config *audit.BackendConfig) (audit.Backend, error) { + noop = &vault.NoopAudit{ + Config: config, + } + return noop, nil + }, + }, }, -} - -func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { - cluster := vault.NewTestCluster(t, loginMFACoreConfig, &vault.TestClusterOptions{ - HandlerFunc: vaulthttp.Handler, - }) + &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) cluster.Start() defer cluster.Cleanup() client := cluster.Cores[0].Client + // Enable the audit backend + err := client.Sys().EnableAuditWithOptions("noop", &api.EnableAuditOptions{Type: "noop"}) + if err != nil { + t.Fatal(err) + } + // Mount the TOTP backend mountInfo := &api.MountInput{ Type: "totp", } - err := client.Sys().Mount("totp", mountInfo) + err = client.Sys().Mount("totp", mountInfo) if err != nil { t.Fatalf("failed to mount totp backend: %v", err) } @@ -254,6 +271,24 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { 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", From a089d24653d0690fc2609d71c0106085799b4ad6 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Thu, 17 Feb 2022 11:29:54 -0800 Subject: [PATCH 10/17] minor fixes --- command/write.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/command/write.go b/command/write.go index 41e1da61051d..eaac9b614d72 100644 --- a/command/write.go +++ b/command/write.go @@ -152,10 +152,10 @@ WRITE: } if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil { - c.UI.Warn(wrapAtLength("WARNING! A login request was issued that is subject to "+ + c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+ "MFA validation. Please make sure to validate the login by sending another "+ "request to mfa/validate endpoint.") + "\n") - ok := YesNoPrompt("Would you like to interactively validate MFA?", true) + ok := YesNoPrompt("Would you like to interactively validate MFA methods?", true) if ok { mfaPayload := make(map[string][]string, 0) for name, mfaConstraintAny := range secret.Auth.MFARequirement.MFAConstraints { @@ -241,5 +241,6 @@ func PasswordPrompt(label string) (string, error) { if err != nil { return "", fmt.Errorf("failed to read the password") } + fmt.Println() return string(b), nil } From ab310468c3b35ad083792ba5feb2edd0d6fa588f Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Thu, 17 Feb 2022 11:39:21 -0800 Subject: [PATCH 11/17] bail if no input was inserted --- command/write.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/command/write.go b/command/write.go index eaac9b614d72..6a45fa2edc5d 100644 --- a/command/write.go +++ b/command/write.go @@ -177,6 +177,11 @@ WRITE: mfaPayload[mfaMethodID] = []string{passcode} } + if len(mfaPayload) == 0 { + c.UI.Error("did not get any input, please validate the login by sending a request to mfa/validate") + goto SKIP + } + // updating data and path data = map[string]interface{}{ "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, From b58be84c3179b59e0eb91766db7572f0cb95789a Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Thu, 17 Feb 2022 17:46:41 -0800 Subject: [PATCH 12/17] change label name --- command/write.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/command/write.go b/command/write.go index 6a45fa2edc5d..6e902f0f69bc 100644 --- a/command/write.go +++ b/command/write.go @@ -166,12 +166,12 @@ WRITE: mfaMethodID := StringPrompt(fmt.Sprintf("From MFARequirement %q, please select one of the following methodIDs %q:\n", name, methodIDs)) if mfaMethodID == "" { c.UI.Warn("Invalid method ID detected, please validate the login by sending a request to mfa/validate") - goto SKIP + goto OUTPUT } passcode, err := PasswordPrompt(fmt.Sprintf("Please insert the passcode for methodID %q: ", mfaMethodID)) if err != nil { c.UI.Error(fmt.Sprintf("Failed to read the passcode with error %q. please validate the login by sending a request to mfa/validate.", err.Error())) - goto SKIP + goto OUTPUT } // passcode could be an empty string mfaPayload[mfaMethodID] = []string{passcode} @@ -179,7 +179,7 @@ WRITE: if len(mfaPayload) == 0 { c.UI.Error("did not get any input, please validate the login by sending a request to mfa/validate") - goto SKIP + goto OUTPUT } // updating data and path @@ -192,7 +192,7 @@ WRITE: } } -SKIP: +OUTPUT: // Handle single field output if c.flagField != "" { return PrintRawField(c.UI, secret, c.flagField) From 2c22714cd1d634bb5024a5e4c477179cea1b877d Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Fri, 18 Feb 2022 19:10:10 -0800 Subject: [PATCH 13/17] interactive CLI when single methodID is returned from login request --- command/base.go | 8 +++ command/write.go | 135 +++++++++++++++++++++-------------------------- 2 files changed, 67 insertions(+), 76 deletions(-) diff --git a/command/base.go b/command/base.go index 558ec4993681..ab5caee7c15c 100644 --- a/command/base.go +++ b/command/base.go @@ -55,6 +55,7 @@ type BaseCommand struct { flagFormat string flagField string flagOutputCurlString bool + flagNonInteractive bool flagMFA []string @@ -393,6 +394,13 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { "This can be specified multiple times.", }) + f.BoolVar(&BoolVar{ + Name: "non-interactive", + Target: &c.flagNonInteractive, + Default: false, + Usage: "It controls a command to be executed in an interactive or non-interactive fashion with a user.", + }) + } if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 { diff --git a/command/write.go b/command/write.go index 6e902f0f69bc..0987cd185da9 100644 --- a/command/write.go +++ b/command/write.go @@ -1,15 +1,12 @@ package command import ( - "bufio" "fmt" "io" "os" "strings" - "syscall" - - "golang.org/x/term" + "github.com/hashicorp/vault/sdk/logical" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -134,7 +131,6 @@ func (c *WriteCommand) Run(args []string) int { return 2 } -WRITE: secret, err := client.Logical().Write(path, data) if err != nil { c.UI.Error(fmt.Sprintf("Error writing data to %s: %s", path, err)) @@ -152,47 +148,19 @@ WRITE: } if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil { + if len(secret.Auth.MFARequirement.MFAConstraints) == 1 && !c.flagNonInteractive { + // Currently, if there is only one MFA method configured, the login + // request is validated interactively + methodID, usePasscode := c.getMethodIDUsePasscode(secret.Auth.MFARequirement.MFAConstraints) + if methodID != "" { + return c.validateMFA(secret.Auth.MFARequirement.MFARequestID, methodID, usePasscode) + } + } c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+ "MFA validation. Please make sure to validate the login by sending another "+ "request to mfa/validate endpoint.") + "\n") - ok := YesNoPrompt("Would you like to interactively validate MFA methods?", true) - if ok { - mfaPayload := make(map[string][]string, 0) - for name, mfaConstraintAny := range secret.Auth.MFARequirement.MFAConstraints { - methodIDs := make([]string, 0) - for _, m := range mfaConstraintAny.Any { - methodIDs = append(methodIDs, m.ID) - } - mfaMethodID := StringPrompt(fmt.Sprintf("From MFARequirement %q, please select one of the following methodIDs %q:\n", name, methodIDs)) - if mfaMethodID == "" { - c.UI.Warn("Invalid method ID detected, please validate the login by sending a request to mfa/validate") - goto OUTPUT - } - passcode, err := PasswordPrompt(fmt.Sprintf("Please insert the passcode for methodID %q: ", mfaMethodID)) - if err != nil { - c.UI.Error(fmt.Sprintf("Failed to read the passcode with error %q. please validate the login by sending a request to mfa/validate.", err.Error())) - goto OUTPUT - } - // passcode could be an empty string - mfaPayload[mfaMethodID] = []string{passcode} - } - - if len(mfaPayload) == 0 { - c.UI.Error("did not get any input, please validate the login by sending a request to mfa/validate") - goto OUTPUT - } - - // updating data and path - data = map[string]interface{}{ - "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, - "mfa_payload": mfaPayload, - } - path = "sys/mfa/validate" - goto WRITE - } } -OUTPUT: // Handle single field output if c.flagField != "" { return PrintRawField(c.UI, secret, c.flagField) @@ -201,51 +169,66 @@ OUTPUT: return OutputSecret(c.UI, secret) } -func StringPrompt(label string) string { - var s string - r := bufio.NewReader(os.Stdin) - for { - fmt.Fprint(os.Stderr, label+" ") - s, _ = r.ReadString('\n') - if s != "" { - break +func (c *WriteCommand) getMethodIDUsePasscode(mfaConstraintAny map[string]*logical.MFAConstraintAny) (string, bool) { + for _, mfaConstraint := range mfaConstraintAny { + if len(mfaConstraint.Any) != 1 { + return "", false } + return mfaConstraint.Any[0].ID, mfaConstraint.Any[0].UsesPasscode } - return strings.TrimSpace(s) + return "", false } -func YesNoPrompt(label string, def bool) bool { - choices := "Y/n" - if !def { - choices = "y/N" +func (c *WriteCommand) validateMFA(reqID, mfaMethodID string, usePasscode bool) int { + var passcode string + var err error + if usePasscode { + passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %s:", mfaMethodID)) + if err != nil { + c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to mfa/validate", err.Error())) + return 2 + } + } else { + // TODO: not sure about printing this message; an error might occur before or during the validate request + c.UI.Warn("Please acknowledge the push notification in your authenticator app") } - r := bufio.NewReader(os.Stdin) - var s string + // passcode could be an empty string + mfaPayload := map[string][]string{ + mfaMethodID: {passcode}, + } - for { - fmt.Fprintf(os.Stderr, "%s (%s) ", label, choices) - s, _ = r.ReadString('\n') - s = strings.TrimSpace(s) - if s == "" { - return def - } - s = strings.ToLower(s) - if s == "y" || s == "yes" { - return true + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := "sys/mfa/validate" + + secret, err := client.Logical().Write(path, map[string]interface{}{ + "mfa_request_id": reqID, + "mfa_payload": mfaPayload, + }) + if err != nil { + c.UI.Error(err.Error()) + if secret != nil { + OutputSecret(c.UI, secret) } - if s == "n" || s == "no" { - return false + return 2 + } + if secret == nil { + // Don't output anything unless using the "table" format + if Format(c.UI) == "table" { + c.UI.Info(fmt.Sprintf("Success! Data written to: %s", path)) } + return 0 } -} -func PasswordPrompt(label string) (string, error) { - fmt.Fprint(os.Stderr, label+" ") - b, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - return "", fmt.Errorf("failed to read the password") + // Handle single field output + if c.flagField != "" { + return PrintRawField(c.UI, secret, c.flagField) } - fmt.Println() - return string(b), nil + + return OutputSecret(c.UI, secret) } From f33a206b277bfea93aece51c4561b000c96387a3 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Fri, 18 Feb 2022 19:13:45 -0800 Subject: [PATCH 14/17] minor fix --- command/write.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/command/write.go b/command/write.go index 0987cd185da9..e5a5e9b04692 100644 --- a/command/write.go +++ b/command/write.go @@ -183,7 +183,7 @@ func (c *WriteCommand) validateMFA(reqID, mfaMethodID string, usePasscode bool) var passcode string var err error if usePasscode { - passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %s:", mfaMethodID)) + passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q:", mfaMethodID)) if err != nil { c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to mfa/validate", err.Error())) return 2 From d4570ba95c61e70cb4df0e629422278f4a3f1491 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Tue, 22 Feb 2022 09:01:19 -0800 Subject: [PATCH 15/17] adding changelog --- changelog/14131.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/14131.txt diff --git a/changelog/14131.txt b/changelog/14131.txt new file mode 100644 index 000000000000..e2d2d87b8688 --- /dev/null +++ b/changelog/14131.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cli: interactive CLI for login mfa +``` From eeb0da85456677076b9092baa44e2875ac37a088 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Tue, 22 Feb 2022 10:44:43 -0800 Subject: [PATCH 16/17] addressing feedback --- command/base.go | 2 +- command/write.go | 42 ++++++++++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/command/base.go b/command/base.go index ab5caee7c15c..9521e17225b6 100644 --- a/command/base.go +++ b/command/base.go @@ -398,7 +398,7 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { Name: "non-interactive", Target: &c.flagNonInteractive, Default: false, - Usage: "It controls a command to be executed in an interactive or non-interactive fashion with a user.", + Usage: "When set true, prevents asking the user for input via the terminal.", }) } diff --git a/command/write.go b/command/write.go index e5a5e9b04692..f8b949b4764f 100644 --- a/command/write.go +++ b/command/write.go @@ -16,6 +16,13 @@ var ( _ cli.CommandAutocomplete = (*WriteCommand)(nil) ) +// MFAMethodInfo contains the information about an MFA method +type MFAMethodInfo struct { + methodID string + methodType string + usePasscode bool +} + // WriteCommand is a Command that puts data into the Vault. type WriteCommand struct { *BaseCommand @@ -151,14 +158,14 @@ func (c *WriteCommand) Run(args []string) int { if len(secret.Auth.MFARequirement.MFAConstraints) == 1 && !c.flagNonInteractive { // Currently, if there is only one MFA method configured, the login // request is validated interactively - methodID, usePasscode := c.getMethodIDUsePasscode(secret.Auth.MFARequirement.MFAConstraints) - if methodID != "" { - return c.validateMFA(secret.Auth.MFARequirement.MFARequestID, methodID, usePasscode) + methodInfo := c.getMFAMethodInfo(secret.Auth.MFARequirement.MFAConstraints) + if methodInfo.methodID != "" { + return c.validateMFA(secret.Auth.MFARequirement.MFARequestID, methodInfo) } } c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+ "MFA validation. Please make sure to validate the login by sending another "+ - "request to mfa/validate endpoint.") + "\n") + "request to sys/mfa/validate endpoint.") + "\n") } // Handle single field output @@ -169,33 +176,40 @@ func (c *WriteCommand) Run(args []string) int { return OutputSecret(c.UI, secret) } -func (c *WriteCommand) getMethodIDUsePasscode(mfaConstraintAny map[string]*logical.MFAConstraintAny) (string, bool) { +// getMFAMethodInfo returns MFA method information only if one MFA method is +// configured. +func (c *WriteCommand) getMFAMethodInfo(mfaConstraintAny map[string]*logical.MFAConstraintAny) MFAMethodInfo { for _, mfaConstraint := range mfaConstraintAny { if len(mfaConstraint.Any) != 1 { - return "", false + return MFAMethodInfo{} + } + + return MFAMethodInfo{ + methodType: mfaConstraint.Any[0].Type, + methodID: mfaConstraint.Any[0].ID, + usePasscode: mfaConstraint.Any[0].UsesPasscode, } - return mfaConstraint.Any[0].ID, mfaConstraint.Any[0].UsesPasscode } - return "", false + + return MFAMethodInfo{} } -func (c *WriteCommand) validateMFA(reqID, mfaMethodID string, usePasscode bool) int { +func (c *WriteCommand) validateMFA(reqID string, methodInfo MFAMethodInfo) int { var passcode string var err error - if usePasscode { - passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q:", mfaMethodID)) + if methodInfo.usePasscode { + passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q of type %q:", methodInfo.methodID, methodInfo.methodType)) if err != nil { - c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to mfa/validate", err.Error())) + c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to sys/mfa/validate", err.Error())) return 2 } } else { - // TODO: not sure about printing this message; an error might occur before or during the validate request c.UI.Warn("Please acknowledge the push notification in your authenticator app") } // passcode could be an empty string mfaPayload := map[string][]string{ - mfaMethodID: {passcode}, + methodInfo.methodID: {passcode}, } client, err := c.Client() From dc9717c1fb65bd8436e13d264d5a6f59939760c9 Mon Sep 17 00:00:00 2001 From: hamid ghaf Date: Thu, 24 Feb 2022 11:53:25 -0800 Subject: [PATCH 17/17] a user with a terminal should be able to choose between interactive and non-interactive. A user without a terminal should not be able to use the interactive mode. --- command/write.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/command/write.go b/command/write.go index f8b949b4764f..0de7299eb8a6 100644 --- a/command/write.go +++ b/command/write.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/hashicorp/vault/sdk/logical" + "github.com/mattn/go-isatty" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -155,7 +156,7 @@ func (c *WriteCommand) Run(args []string) int { } if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil { - if len(secret.Auth.MFARequirement.MFAConstraints) == 1 && !c.flagNonInteractive { + if c.isInteractiveEnabled(len(secret.Auth.MFARequirement.MFAConstraints)) { // Currently, if there is only one MFA method configured, the login // request is validated interactively methodInfo := c.getMFAMethodInfo(secret.Auth.MFARequirement.MFAConstraints) @@ -176,6 +177,18 @@ func (c *WriteCommand) Run(args []string) int { return OutputSecret(c.UI, secret) } +func (c *WriteCommand) isInteractiveEnabled(mfaConstraintLen int) bool { + if mfaConstraintLen != 1 || !isatty.IsTerminal(os.Stdin.Fd()) { + return false + } + + if !c.flagNonInteractive { + return true + } + + return false +} + // getMFAMethodInfo returns MFA method information only if one MFA method is // configured. func (c *WriteCommand) getMFAMethodInfo(mfaConstraintAny map[string]*logical.MFAConstraintAny) MFAMethodInfo { @@ -204,7 +217,8 @@ func (c *WriteCommand) validateMFA(reqID string, methodInfo MFAMethodInfo) int { return 2 } } else { - c.UI.Warn("Please acknowledge the push notification in your authenticator app") + c.UI.Warn("Asking Vault to perform MFA validation with upstream service. " + + "You should receive a push notification in your authenticator app shortly") } // passcode could be an empty string