[VAULT-38601] Modify response to MFA enforced requests to enable TOTP self-enrollment (#8723) (#8746)

Co-authored-by: Kuba Wieczorek <kuba.wieczorek@hashicorp.com>
This commit is contained in:
Vault Automation 2025-08-20 14:22:00 -06:00 committed by GitHub
parent 3594d6d6b1
commit ae0e5e160f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 134 additions and 52 deletions

View File

@ -298,6 +298,11 @@ type MFAMethodID struct {
ID string `json:"id,omitempty"`
UsesPasscode bool `json:"uses_passcode,omitempty"`
Name string `json:"name,omitempty"`
// SelfEnrollmentEnabled indicates whether the user does not yet have an MFA
// secret for this method and self-enrollment is enabled for it. Clients (like the UI) can use
// this to determine whether to offer the user a way to generate an MFA secret
// for this method.
SelfEnrollmentEnabled bool `json:"self_enrollment_enabled,omitempty"`
}
type MFAConstraintAny struct {

View File

@ -306,13 +306,14 @@ func (x *Group) GetNamespaceID() string {
}
type MFAMethodID struct {
state protoimpl.MessageState `protogen:"open.v1"`
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"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
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"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
SelfEnrollmentEnabled bool `protobuf:"varint,5,opt,name=self_enrollment_enabled,json=selfEnrollmentEnabled,proto3" json:"self_enrollment_enabled,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MFAMethodID) Reset() {
@ -373,6 +374,13 @@ func (x *MFAMethodID) GetName() string {
return ""
}
func (x *MFAMethodID) GetSelfEnrollmentEnabled() bool {
if x != nil {
return x.SelfEnrollmentEnabled
}
return false
}
type MFAConstraintAny struct {
state protoimpl.MessageState `protogen:"open.v1"`
Any []*MFAMethodID `protobuf:"bytes,1,rep,name=any,proto3" json:"any,omitempty"`
@ -531,35 +539,39 @@ var file_sdk_logical_identity_proto_rawDesc = string([]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, 0x22, 0x6a, 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, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 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,
0x01, 0x22, 0xa2, 0x01, 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, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x36,
0x0a, 0x17, 0x73, 0x65, 0x6c, 0x66, 0x5f, 0x65, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e,
0x74, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52,
0x15, 0x73, 0x65, 0x6c, 0x66, 0x45, 0x6e, 0x72, 0x6f, 0x6c, 0x6c, 0x6d, 0x65, 0x6e, 0x74, 0x45,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 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 (

View File

@ -83,6 +83,7 @@ message MFAMethodID {
string id = 2;
bool uses_passcode = 3;
string name = 4;
bool self_enrollment_enabled = 5;
}
message MFAConstraintAny {

View File

@ -1273,7 +1273,7 @@ func (b *LoginMFABackend) mfaConfigReadByMethodID(id string) (map[string]interfa
return nil, nil
}
return b.mfaConfigToMap(mConfig, true)
return b.mfaConfigToMap(mConfig, constants.IsEnterprise, true)
}
func (b *LoginMFABackend) mfaMethodList(ctx context.Context, methodType string) ([]string, map[string]interface{}, error) {
@ -1333,7 +1333,7 @@ func (b *LoginMFABackend) mfaMethodList(ctx context.Context, methodType string)
}
keys = append(keys, config.ID)
configInfoEntry, err := b.mfaConfigToMap(config, true)
configInfoEntry, err := b.mfaConfigToMap(config, constants.IsEnterprise, true)
if err != nil {
return nil, nil, fmt.Errorf("failed to convert config to map: %w", err)
}
@ -1423,7 +1423,7 @@ func (b *LoginMFABackend) mfaLoginEnforcementConfigToMap(eConfig *mfa.MFAEnforce
// method endpoints. The `isLoginMFA` parameter indicates whether the
// configuration is for login MFA, which includes additional fields on the
// shared mfa.Config object for the TOTP type MFA method.
func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config, isLoginMFA bool) (map[string]interface{}, error) {
func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config, isEnterprise, isLoginMFA bool) (map[string]interface{}, error) {
respData := make(map[string]interface{})
switch mConfig.Config.(type) {
@ -1437,9 +1437,9 @@ func (b *MFABackend) mfaConfigToMap(mConfig *mfa.Config, isLoginMFA bool) (map[s
respData["qr_size"] = totpConfig.QRSize
respData["algorithm"] = otplib.Algorithm(totpConfig.Algorithm).String()
respData["max_validation_attempts"] = totpConfig.MaxValidationAttempts
if isLoginMFA {
if isEnterprise && isLoginMFA {
// Login MFA and policy (i.e. enterprise step-up) MFA share the same protobuf message for TOTPConfig,
// but the login MFA has an additional field for self-enrollment.
// but the login MFA has an additional field for self-enrollment, which is an enterprise feature.
respData["enable_self_enrollment"] = totpConfig.GetEnableSelfEnrollment()
}
case *mfa.Config_OktaConfig:

View File

@ -79,6 +79,7 @@ func TestMFAConfigToMap(t *testing.T) {
testCases := map[string]struct {
config *mfa.Config
expectedResult map[string]any
isEnterprise bool
isLoginMFA bool
}{
"totp-with-login-mfa": {
@ -114,7 +115,43 @@ func TestMFAConfigToMap(t *testing.T) {
"skew": uint32(1),
"type": "totp",
},
isLoginMFA: true,
isEnterprise: true,
isLoginMFA: true,
},
"totp-with-login-mfa-non-ent": {
config: &mfa.Config{
Type: mfaMethodTypeTOTP,
Config: &mfa.Config_TOTPConfig{
TOTPConfig: &mfa.TOTPConfig{
Issuer: "TestIssuer",
Period: 30,
Digits: 6,
Skew: 1,
KeySize: 20,
QRSize: 200,
Algorithm: int32(otplib.AlgorithmSHA1),
MaxValidationAttempts: 5,
EnableSelfEnrollment: true,
},
},
},
expectedResult: map[string]interface{}{
"algorithm": "SHA1",
"digits": int32(6),
"id": "",
"issuer": "TestIssuer",
"key_size": uint32(20),
"max_validation_attempts": uint32(5),
"name": "",
"namespace_id": "",
"namespace_path": "/",
"period": uint32(30),
"qr_size": int32(200),
"skew": uint32(1),
"type": "totp",
},
isEnterprise: false,
isLoginMFA: true,
},
"totp-ent-step-up-mfa-self-enrollment-false": {
config: &mfa.Config{
@ -148,7 +185,8 @@ func TestMFAConfigToMap(t *testing.T) {
"skew": uint32(1),
"type": "totp",
},
isLoginMFA: false,
isEnterprise: true,
isLoginMFA: false,
},
"totp-ent-step-up-mfa-self-enrollment-true": {
config: &mfa.Config{
@ -182,7 +220,8 @@ func TestMFAConfigToMap(t *testing.T) {
"skew": uint32(1),
"type": "totp",
},
isLoginMFA: false,
isEnterprise: true,
isLoginMFA: false,
},
"okta-prod": {
config: &mfa.Config{
@ -257,9 +296,8 @@ func TestMFAConfigToMap(t *testing.T) {
"namespace_id": "",
"namespace_path": "/",
},
isLoginMFA: false,
},
"pingid": {
"ping-id": {
config: &mfa.Config{
Type: mfaMethodTypePingID,
Config: &mfa.Config_PingIDConfig{
@ -284,7 +322,6 @@ func TestMFAConfigToMap(t *testing.T) {
"namespace_id": "",
"namespace_path": "/",
},
isLoginMFA: false,
},
}
backend := &MFABackend{}
@ -292,7 +329,7 @@ func TestMFAConfigToMap(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
actualResult, err := backend.mfaConfigToMap(tc.config, tc.isLoginMFA)
actualResult, err := backend.mfaConfigToMap(tc.config, tc.isEnterprise, tc.isLoginMFA)
require.NoError(t, err)
require.Equal(t, tc.expectedResult, actualResult)
})
@ -308,7 +345,7 @@ func TestMfaConfigToMap_InvalidType(t *testing.T) {
Config: nil,
}
_, err := backend.mfaConfigToMap(mConfig, true)
_, err := backend.mfaConfigToMap(mConfig, true, true)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid method type")
}

View File

@ -26,6 +26,7 @@ import (
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/command/server"
"github.com/hashicorp/vault/helper/constants"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/identity/mfa"
"github.com/hashicorp/vault/helper/metricsutil"
@ -1874,7 +1875,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
MFAConstraints: make(map[string]*logical.MFAConstraintAny),
}
for _, eConfig := range matchedMfaEnforcementList {
mfaAny, err := c.buildMfaEnforcementResponse(eConfig)
mfaAny, err := c.buildMfaEnforcementResponse(eConfig, entity)
if err != nil {
return nil, nil, err
}
@ -2456,7 +2457,13 @@ func (c *Core) getUserLockoutFromConfig(mountType string) UserLockoutConfig {
return defaultUserLockoutConfig
}
func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*logical.MFAConstraintAny, error) {
func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig, entity *identity.Entity) (*logical.MFAConstraintAny, error) {
if eConfig == nil {
return nil, fmt.Errorf("MFA enforcement config is nil")
}
if entity == nil {
return nil, fmt.Errorf("entity is nil")
}
mfaAny := &logical.MFAConstraintAny{
Any: []*logical.MFAMethodID{},
}
@ -2469,15 +2476,35 @@ func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*
if mConfig.Type == mfaMethodTypeDuo {
duoConf, ok := mConfig.Config.(*mfa.Config_DuoConfig)
if !ok {
return nil, fmt.Errorf("invalid MFA configuration type")
return nil, fmt.Errorf("invalid MFA configuration type, expected DuoConfig")
}
duoUsePasscode = duoConf.DuoConfig.UsePasscode
}
allowSelfEnrollment := false
if mConfig.Type == mfaMethodTypeTOTP && constants.IsEnterprise {
totpConf, ok := mConfig.Config.(*mfa.Config_TOTPConfig)
if !ok {
return nil, fmt.Errorf("invalid MFA configuration type, expected TOTPConfig")
}
enrollmentEnabled := totpConf.TOTPConfig.GetEnableSelfEnrollment()
_, entityHasMFASecretForMethodID := entity.MFASecrets[methodID]
if enrollmentEnabled && !entityHasMFASecretForMethodID {
// If enable_self_enrollment setting on the TOTP MFA method config is set to
// true and the entity does not have an MFA secret yet, we will allow
// self-service enrollment.
allowSelfEnrollment = true
}
}
mfaMethod := &logical.MFAMethodID{
Type: mConfig.Type,
ID: methodID,
UsesPasscode: mConfig.Type == mfaMethodTypeTOTP || duoUsePasscode,
Name: mConfig.Name,
// This will be used by the client to determine whether it should offer the user
// a way to generate an MFA secret for this method.
SelfEnrollmentEnabled: allowSelfEnrollment,
}
mfaAny.Any = append(mfaAny.Any, mfaMethod)
}