From 2fe214f5719a17b67f2630f82f8e51797db1a633 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Thu, 17 Feb 2022 13:08:51 -0800 Subject: [PATCH] Login MFA (#14025) * Login MFA * ENT OSS segragation (#14088) * 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 * adding use_passcode field to DUO config (#14059) * add changelog * preventing replay attack on MFA passcodes (#14056) * preventing replay attack on MFA passcodes * using %w instead of %s for error * Improve CLI command for login mfa (#14106) CLI prints a warning message indicating the login request needs to get validated * adding the validity period of a passcode to error messages (#14115) * PR feedback * duo to handle preventing passcode reuse Co-authored-by: hghaf099 <83242695+hghaf099@users.noreply.github.com> Co-authored-by: hamid ghaf --- api/client.go | 6 + api/secret.go | 3 + changelog/14025.txt | 3 + command/write.go | 5 + go.mod | 1 + go.sum | 1 + helper/forwarding/types.pb.go | 2 +- helper/identity/mfa/mfa.go | 19 + helper/identity/mfa/types.pb.go | 327 ++- helper/identity/mfa/types.proto | 17 + 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/helper/pluginutil/multiplexing.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 | 73 +- vault/core_util.go | 12 + .../identity/login_mfa_duo_test.go | 290 ++ .../identity/login_mfa_okta_test.go | 354 +++ .../identity/login_mfa_totp_test.go | 291 ++ vault/external_tests/mfa/login_mfa_test.go | 436 +++ vault/identity_store.go | 351 ++- vault/identity_store_oss.go | 4 - vault/identity_store_structs.go | 1 + vault/logical_system.go | 30 +- vault/logical_system_helpers.go | 6 +- vault/login_mfa.go | 2528 +++++++++++++++++ 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 | 303 +- 42 files changed, 5481 insertions(+), 233 deletions(-) create mode 100644 changelog/14025.txt 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 87f8c537e9f1..6a804091a219 100644 --- a/api/client.go +++ b/api/client.go @@ -800,6 +800,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/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. +``` diff --git a/command/write.go b/command/write.go index dd0d7cfde99e..c110e9da26a8 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("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) diff --git a/go.mod b/go.mod index 87dced991782..6800a0e921fd 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 d3714308c352..e3d81de2e7a5 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= diff --git a/helper/forwarding/types.pb.go b/helper/forwarding/types.pb.go index 3a036f4726aa..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.19.4 +// 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 5cb27bea548d..0cfa12fa29a5 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.19.4 +// 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() } @@ -301,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() { @@ -363,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 { @@ -745,12 +763,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 +895,103 @@ 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, 0xb6, 0x01, 0x0a, 0x09, 0x44, 0x75, 0x6f, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, + 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, + 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, + 0x61, 0x70, 0x69, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x48, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x73, 0x68, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x73, 0x68, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, + 0x75, 0x73, 0x65, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x75, 0x73, 0x65, 0x50, 0x61, 0x73, 0x73, 0x63, 0x6f, 0x64, 0x65, 0x22, + 0xa4, 0x01, 0x0a, 0x0a, 0x4f, 0x6b, 0x74, 0x61, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x19, + 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6f, 0x72, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x70, 0x69, + 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x70, + 0x69, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x64, + 0x75, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, + 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x73, 0x65, 0x55, 0x72, + 0x6c, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, + 0x79, 0x45, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0xef, 0x01, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x49, + 0x44, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x75, 0x73, 0x65, 0x5f, 0x62, + 0x61, 0x73, 0x65, 0x36, 0x34, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x75, 0x73, 0x65, 0x42, 0x61, 0x73, 0x65, 0x36, 0x34, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, + 0x0d, 0x75, 0x73, 0x65, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x75, 0x73, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, + 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x64, 0x70, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x64, 0x70, 0x55, 0x72, + 0x6c, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x72, 0x67, 0x5f, 0x61, 0x6c, 0x69, 0x61, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x72, 0x67, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x1b, + 0x0a, 0x09, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x61, 0x64, 0x6d, 0x69, 0x6e, 0x55, 0x72, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x61, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x6c, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x6f, 0x72, 0x55, 0x72, 0x6c, 0x22, 0x66, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x0b, 0x74, 0x6f, 0x74, 0x70, 0x5f, 0x73, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6d, 0x66, 0x61, 0x2e, 0x54, + 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x74, 0x6f, 0x74, + 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x22, 0xd6, 0x01, 0x0a, 0x0a, 0x54, 0x4f, 0x54, 0x50, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x12, + 0x1c, 0x0a, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x09, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x12, 0x16, 0x0a, + 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, + 0x69, 0x67, 0x69, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x04, 0x73, 0x6b, 0x65, 0x77, 0x12, 0x19, 0x0a, 0x08, 0x6b, 0x65, 0x79, + 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x6b, 0x65, 0x79, + 0x53, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xc1, 0x02, 0x0a, 0x14, 0x4d, 0x46, + 0x41, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x66, 0x61, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x0c, 0x6d, 0x66, 0x61, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x73, 0x12, + 0x32, 0x0a, 0x15, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x61, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, + 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, + 0x6f, 0x72, 0x73, 0x12, 0x2a, 0x0a, 0x11, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, + 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, + 0x2c, 0x0a, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, + 0x70, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x64, 0x73, 0x12, 0x2e, 0x0a, + 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x5f, 0x69, 0x64, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x49, 0x64, 0x73, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x42, 0x30, 0x5a, + 0x2e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x76, 0x61, 0x75, 0x6c, 0x74, 0x2f, 0x68, 0x65, 0x6c, 0x70, + 0x65, 0x72, 0x2f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2f, 0x6d, 0x66, 0x61, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -858,15 +1006,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 +1120,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 +1148,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..c20386cb9011 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 @@ -61,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 @@ -129,3 +133,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 a392d24bc313..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.19.4 +// 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 bd7b780cd5a9..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.19.4 +// 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 5fca8f6c3e81..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.19.4 +// 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 7c9e08a9b03e..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.19.4 +// 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 3699a9d662ff..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.19.4 +// protoc v3.19.3 // source: sdk/database/dbplugin/v5/proto/database.proto package proto diff --git a/sdk/helper/pluginutil/multiplexing.pb.go b/sdk/helper/pluginutil/multiplexing.pb.go index d0ff51e57b24..fa3357d49045 100644 --- a/sdk/helper/pluginutil/multiplexing.pb.go +++ b/sdk/helper/pluginutil/multiplexing.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.19.4 +// protoc v3.19.3 // source: sdk/helper/pluginutil/multiplexing.proto package pluginutil 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 0a68eadf69d3..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.19.4 +// 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 b16f0a75af97..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.19.4 +// 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 dbad4da977ce..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.19.4 +// 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 5388f9f78670..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.19.4 +// protoc v3.19.3 // source: vault/activity/activity_log.proto package activity diff --git a/vault/core.go b/vault/core.go index 0f8d9db04d98..1bca8a2cea35 100644 --- a/vault/core.go +++ b/vault/core.go @@ -75,6 +75,10 @@ const ( 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 + // ForwardSSCTokenToActive is the value that must be set in the // forwardToActive to trigger forwarding if a perf standby encounters // an SSC Token that it does not have the WAL state for. @@ -340,7 +344,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 @@ -377,6 +382,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 @@ -994,6 +1003,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 @@ -2086,9 +2097,13 @@ func (s standardUnsealStrategy) unseal(ctx context.Context, logger log.Logger, c if err := c.setupQuotas(ctx, false); err != nil { return err } + + c.setupCachedMFAResponseAuth() + if err := c.setupHeaderHMACKey(ctx, false); err != nil { return err } + if !c.IsDRSecondary() { if err := c.startRollback(); err != nil { return err @@ -2259,6 +2274,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)) } @@ -2998,6 +3018,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 99a0f8d9871f..e057c35a2526 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("failed 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_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..dd7b25628ea5 --- /dev/null +++ b/vault/external_tests/identity/login_mfa_totp_test.go @@ -0,0 +1,291 @@ +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" + 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, + }, +} + +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/totp", map[string]interface{}{ + "issuer": "yCorp", + "period": 5, + "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/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 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") + } + 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 + // 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{ + 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") + } + + _, err = user2Client.Logical().Write("sys/mfa/validate", map[string]interface{}{ + "mfa_request_id": secret.Auth.MFARequirement.MFARequestID, + "mfa_payload": map[string][]string{ + methodID: {totpPasscode}, + }, + }) + if err == nil { + t.Fatalf("MFA succeeded with an already used passcode") + } + if !strings.Contains(err.Error(), "code already used") { + t.Fatalf("expected error message to mention code already used") + } + + // Destroy the secret so that the token can self generate + _, err = userClient.Logical().Write(fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{ + "entity_id": entityID, + "method_id": methodID, + }) + if err != nil { + t.Fatalf("failed to destroy the MFA secret: %s", err) + } + } +} 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..fc19c8b8bc17 --- /dev/null +++ b/vault/external_tests/mfa/login_mfa_test.go @@ -0,0 +1,436 @@ +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_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") + } +} + +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..f0b6de7973e0 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,357 @@ 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.", + }, + "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{ + 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_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/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 3b9aad9c8806..550dc6028c10 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" @@ -68,19 +68,20 @@ func systemBackendMemDBSchema() *memdb.DBSchema { return systemSchema } +type PolicyMFABackend struct { + *MFABackend +} + 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) - b.Backend = &framework.Backend{ Help: strings.TrimSpace(sysHelpRoot), @@ -147,6 +148,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{ @@ -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()...) @@ -223,11 +226,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..f2ea5339ecbe --- /dev/null +++ b/vault/login_mfa.go @@ -0,0 +1,2528 @@ +package vault + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "image/png" + "io/ioutil" + "net/http" + "net/url" + "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/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" + "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" + "github.com/patrickmn/go-cache" + 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/: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 + usedCodes *cache.Cache +} + +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, + usedCodes: cache.New(0, 30*time.Second), + } +} + +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(fmt.Sprintf("failed to satisfy enforcement %s. error: %s", eConfig.Name, err.Error())), 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 (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), + UsePasscode: d.Get("use_passcode").(bool), + } + + 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 + respData["use_passcode"] = duoConfig.UsePasscode + 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") + } + + // 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; new code is available in %v seconds", validityPeriod) + } + + 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") + } + + // Adding the used code to the cache + err = c.loginMFABackend.usedCodes.Add(usedName, nil, validityPeriod) + if err != nil { + return fmt.Errorf("error adding code to used cache: %w", err) + } + + return nil +} + +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, err := b.Core.barrierViewForNamespace(eConfig.NamespaceID) + if err != nil { + return err + } + + 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() + + 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) + 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, err := b.Core.barrierViewForNamespace(mConfig.NamespaceID) + if err != nil { + return err + } + 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, err := b.Core.barrierViewForNamespace(eConfig.NamespaceID) + if err != nil { + return err + } + + return barrierView.Put(ctx, &logical.StorageEntry{ + Key: entryIndex, + Value: marshaledEntry, + }) +} + +func (b *LoginMFABackend) getMFALoginEnforcementConfig(ctx context.Context, key, namespaceId string) (*mfa.MFAEnforcementConfig, error) { + 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 + } + 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 +} + +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 d16aa5d07155..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.19.4 +// 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 61f7a3bf0892..e9dc26f96ffb 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -9,13 +9,15 @@ import ( "strings" "time" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/golang/protobuf/proto" "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" @@ -1349,7 +1351,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 @@ -1447,94 +1449,105 @@ 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 } + // 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") + } + + // 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 + } - 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 - } - } - - 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 - } - - 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: mfaRequestID, + } + err = c.SaveMFAResponseAuth(respAuth) + if err != nil { + return nil, nil, err + } + auth = nil + 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 + return resp, auth, nil } - 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. @@ -1557,6 +1570,134 @@ 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) + } + 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 || duoUsePasscode, + } + 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