mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
Store login MFA secret with tokenhelper (#17040)
* Store login MFA secret with tokenhelper * Clean up and refactor tokenhelper paths * Refactor totp test code for re-use * Add login MFA command tests * Use longer sleep times and sha512 for totp test * Add changelog
This commit is contained in:
3
changelog/17040.txt
Normal file
3
changelog/17040.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:bug
|
||||||
|
login: Store token in tokenhelper for interactive login MFA
|
||||||
|
```
|
||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"github.com/hashicorp/vault/command/token"
|
"github.com/hashicorp/vault/command/token"
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -220,44 +219,55 @@ func (c *BaseCommand) DefaultWrappingLookupFunc(operation, path string) string {
|
|||||||
return api.DefaultWrappingLookupFunc(operation, path)
|
return api.DefaultWrappingLookupFunc(operation, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BaseCommand) isInteractiveEnabled(mfaConstraintLen int) bool {
|
// getValidationRequired checks to see if the secret exists and has an MFA
|
||||||
if mfaConstraintLen != 1 || !isatty.IsTerminal(os.Stdin.Fd()) {
|
// requirement. If MFA is required and the number of constraints is greater than
|
||||||
return false
|
// 1, we can assert that interactive validation is not required.
|
||||||
}
|
func (c *BaseCommand) getMFAValidationRequired(secret *api.Secret) bool {
|
||||||
|
if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil {
|
||||||
if !c.flagNonInteractive {
|
if c.flagMFA == nil && len(secret.Auth.MFARequirement.MFAConstraints) == 1 {
|
||||||
return true
|
return true
|
||||||
|
} else if len(secret.Auth.MFARequirement.MFAConstraints) > 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMFAMethodInfo returns MFA method information only if one MFA method is
|
// getInteractiveMFAMethodInfo returns MFA method information only if operating
|
||||||
// configured.
|
// in interactive mode and one MFA method is configured.
|
||||||
func (c *BaseCommand) getMFAMethodInfo(mfaConstraintAny map[string]*logical.MFAConstraintAny) MFAMethodInfo {
|
func (c *BaseCommand) getInteractiveMFAMethodInfo(secret *api.Secret) *MFAMethodInfo {
|
||||||
for _, mfaConstraint := range mfaConstraintAny {
|
if secret == nil || secret.Auth == nil || secret.Auth.MFARequirement == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mfaConstraints := secret.Auth.MFARequirement.MFAConstraints
|
||||||
|
if c.flagNonInteractive || len(mfaConstraints) != 1 || !isatty.IsTerminal(os.Stdin.Fd()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, mfaConstraint := range mfaConstraints {
|
||||||
if len(mfaConstraint.Any) != 1 {
|
if len(mfaConstraint.Any) != 1 {
|
||||||
return MFAMethodInfo{}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return MFAMethodInfo{
|
return &MFAMethodInfo{
|
||||||
methodType: mfaConstraint.Any[0].Type,
|
methodType: mfaConstraint.Any[0].Type,
|
||||||
methodID: mfaConstraint.Any[0].ID,
|
methodID: mfaConstraint.Any[0].ID,
|
||||||
usePasscode: mfaConstraint.Any[0].UsesPasscode,
|
usePasscode: mfaConstraint.Any[0].UsesPasscode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MFAMethodInfo{}
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BaseCommand) validateMFA(reqID string, methodInfo MFAMethodInfo) int {
|
func (c *BaseCommand) validateMFA(reqID string, methodInfo MFAMethodInfo) (*api.Secret, error) {
|
||||||
var passcode string
|
var passcode string
|
||||||
var err error
|
var err error
|
||||||
if methodInfo.usePasscode {
|
if methodInfo.usePasscode {
|
||||||
passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q of type %q:", methodInfo.methodID, methodInfo.methodType))
|
passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q of type %q:", methodInfo.methodID, methodInfo.methodType))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to sys/mfa/validate", err.Error()))
|
return nil, fmt.Errorf("failed to read passphrase: %w. please validate the login by sending a request to sys/mfa/validate", err)
|
||||||
return 2
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.UI.Warn("Asking Vault to perform MFA validation with upstream service. " +
|
c.UI.Warn("Asking Vault to perform MFA validation with upstream service. " +
|
||||||
@@ -271,32 +281,10 @@ func (c *BaseCommand) validateMFA(reqID string, methodInfo MFAMethodInfo) int {
|
|||||||
|
|
||||||
client, err := c.Client()
|
client, err := c.Client()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.UI.Error(err.Error())
|
return nil, err
|
||||||
return 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := client.Sys().MFAValidate(reqID, mfaPayload)
|
return client.Sys().MFAValidate(reqID, mfaPayload)
|
||||||
if err != nil {
|
|
||||||
c.UI.Error(err.Error())
|
|
||||||
if secret != nil {
|
|
||||||
OutputSecret(c.UI, secret)
|
|
||||||
}
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if secret == nil {
|
|
||||||
// Don't output anything unless using the "table" format
|
|
||||||
if Format(c.UI) == "table" {
|
|
||||||
c.UI.Info("Success! Data written to: sys/mfa/validate")
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle single field output
|
|
||||||
if c.flagField != "" {
|
|
||||||
return PrintRawField(c.UI, secret, c.flagField)
|
|
||||||
}
|
|
||||||
|
|
||||||
return OutputSecret(c.UI, secret)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlagSetBit uint
|
type FlagSetBit uint
|
||||||
|
|||||||
@@ -228,21 +228,27 @@ func (c *LoginCommand) Run(args []string) int {
|
|||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil {
|
// If there is only one MFA method configured and c.NonInteractive flag is
|
||||||
if c.isInteractiveEnabled(len(secret.Auth.MFARequirement.MFAConstraints)) {
|
// unset, the login request is validated interactively.
|
||||||
// Currently, if there is only one MFA method configured, the login
|
//
|
||||||
// request is validated interactively
|
// interactiveMethodInfo here means that `validateMFA` will complete the MFA
|
||||||
methodInfo := c.getMFAMethodInfo(secret.Auth.MFARequirement.MFAConstraints)
|
// by prompting for a password or directing you to a push notification. In
|
||||||
if methodInfo.methodID != "" {
|
// this scenario, no external validation is needed.
|
||||||
return c.validateMFA(secret.Auth.MFARequirement.MFARequestID, methodInfo)
|
interactiveMethodInfo := c.getInteractiveMFAMethodInfo(secret)
|
||||||
}
|
if interactiveMethodInfo != nil {
|
||||||
|
c.UI.Warn("Initiating Iteractive MFA Validation...")
|
||||||
|
secret, err = c.validateMFA(secret.Auth.MFARequirement.MFARequestID, *interactiveMethodInfo)
|
||||||
|
if err != nil {
|
||||||
|
c.UI.Error(err.Error())
|
||||||
|
return 2
|
||||||
}
|
}
|
||||||
|
} else if c.getMFAValidationRequired(secret) {
|
||||||
|
// Warn about existing login token, but return here, since the secret
|
||||||
|
// won't have any token information if further validation is required.
|
||||||
|
c.checkForAndWarnAboutLoginToken()
|
||||||
c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+
|
c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+
|
||||||
"MFA validation. Please make sure to validate the login by sending another "+
|
"MFA validation. Please make sure to validate the login by sending another "+
|
||||||
"request to sys/mfa/validate endpoint.") + "\n")
|
"request to sys/mfa/validate endpoint.") + "\n")
|
||||||
|
|
||||||
// We return early to prevent success message from being printed
|
|
||||||
c.checkForAndWarnAboutLoginToken()
|
|
||||||
return OutputSecret(c.UI, secret)
|
return OutputSecret(c.UI, secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
credToken "github.com/hashicorp/vault/builtin/credential/token"
|
credToken "github.com/hashicorp/vault/builtin/credential/token"
|
||||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||||
"github.com/hashicorp/vault/command/token"
|
"github.com/hashicorp/vault/command/token"
|
||||||
|
"github.com/hashicorp/vault/helper/testhelpers"
|
||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -428,6 +429,91 @@ func TestLoginCommand_Run(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("login_mfa_single_phase", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, closer := testVaultServer(t)
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
ui, cmd := testLoginCommand(t)
|
||||||
|
|
||||||
|
userclient, entityID, methodID := testhelpers.SetupLoginMFATOTP(t, client)
|
||||||
|
cmd.client = userclient
|
||||||
|
|
||||||
|
enginePath := testhelpers.RegisterEntityInTOTPEngine(t, client, entityID, methodID)
|
||||||
|
totpCode := testhelpers.GetTOTPCodeFromEngine(t, client, enginePath)
|
||||||
|
|
||||||
|
// login command bails early for test clients, so we have to explicitly set this
|
||||||
|
cmd.client.SetMFACreds([]string{methodID + ":" + totpCode})
|
||||||
|
code := cmd.Run([]string{
|
||||||
|
"-method", "userpass",
|
||||||
|
"username=testuser1",
|
||||||
|
"password=testpassword",
|
||||||
|
})
|
||||||
|
if exp := 0; code != exp {
|
||||||
|
t.Errorf("expected %d to be %d", code, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenHelper, err := cmd.TokenHelper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
storedToken, err := tokenHelper.Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
output = ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||||
|
t.Logf("\n%+v", output)
|
||||||
|
if !strings.Contains(output, storedToken) {
|
||||||
|
t.Fatalf("expected stored token: %q, got: %q", storedToken, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("login_mfa_two_phase", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client, closer := testVaultServer(t)
|
||||||
|
defer closer()
|
||||||
|
|
||||||
|
ui, cmd := testLoginCommand(t)
|
||||||
|
|
||||||
|
userclient, entityID, methodID := testhelpers.SetupLoginMFATOTP(t, client)
|
||||||
|
cmd.client = userclient
|
||||||
|
|
||||||
|
_ = testhelpers.RegisterEntityInTOTPEngine(t, client, entityID, methodID)
|
||||||
|
|
||||||
|
// clear the MFA creds just to be sure
|
||||||
|
cmd.client.SetMFACreds([]string{})
|
||||||
|
|
||||||
|
code := cmd.Run([]string{
|
||||||
|
"-method", "userpass",
|
||||||
|
"username=testuser1",
|
||||||
|
"password=testpassword",
|
||||||
|
})
|
||||||
|
if exp := 0; code != exp {
|
||||||
|
t.Errorf("expected %d to be %d", code, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := methodID
|
||||||
|
output = ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||||
|
t.Logf("\n%+v", output)
|
||||||
|
if !strings.Contains(output, expected) {
|
||||||
|
t.Fatalf("expected stored token: %q, got: %q", expected, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenHelper, err := cmd.TokenHelper()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
storedToken, err := tokenHelper.Get()
|
||||||
|
if storedToken != "" {
|
||||||
|
t.Fatal("expected empty stored token")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("communication_failure", func(t *testing.T) {
|
t.Run("communication_failure", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -158,15 +158,16 @@ func handleWriteSecretOutput(c *BaseCommand, path string, secret *api.Secret, er
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if secret != nil && secret.Auth != nil && secret.Auth.MFARequirement != nil {
|
// Currently, if there is only one MFA method configured, the login
|
||||||
if c.isInteractiveEnabled(len(secret.Auth.MFARequirement.MFAConstraints)) {
|
// request is validated interactively
|
||||||
// Currently, if there is only one MFA method configured, the login
|
methodInfo := c.getInteractiveMFAMethodInfo(secret)
|
||||||
// request is validated interactively
|
if methodInfo != nil {
|
||||||
methodInfo := c.getMFAMethodInfo(secret.Auth.MFARequirement.MFAConstraints)
|
secret, err = c.validateMFA(secret.Auth.MFARequirement.MFARequestID, *methodInfo)
|
||||||
if methodInfo.methodID != "" {
|
if err != nil {
|
||||||
return c.validateMFA(secret.Auth.MFARequirement.MFARequestID, methodInfo)
|
c.UI.Error(err.Error())
|
||||||
}
|
return 2
|
||||||
}
|
}
|
||||||
|
} else if c.getMFAValidationRequired(secret) {
|
||||||
c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+
|
c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+
|
||||||
"MFA validation. Please make sure to validate the login by sending another "+
|
"MFA validation. Please make sure to validate the login by sending another "+
|
||||||
"request to sys/mfa/validate endpoint.") + "\n")
|
"request to sys/mfa/validate endpoint.") + "\n")
|
||||||
|
|||||||
@@ -779,3 +779,200 @@ func RetryUntil(t testing.T, timeout time.Duration, f func() error) {
|
|||||||
}
|
}
|
||||||
t.Fatalf("did not complete before deadline, err: %v", err)
|
t.Fatalf("did not complete before deadline, err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateEntityAndAlias clones an existing client and creates an entity/alias.
|
||||||
|
// It returns the cloned client, entityID, and aliasID.
|
||||||
|
func CreateEntityAndAlias(t testing.T, client *api.Client, mountAccessor, entityName, aliasName string) (*api.Client, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
userClient, err := client.Clone()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to clone the client:%v", err)
|
||||||
|
}
|
||||||
|
userClient.SetToken(client.Token())
|
||||||
|
|
||||||
|
resp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity", map[string]interface{}{
|
||||||
|
"name": entityName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create an entity:%v", err)
|
||||||
|
}
|
||||||
|
entityID := resp.Data["id"].(string)
|
||||||
|
|
||||||
|
aliasResp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity-alias", map[string]interface{}{
|
||||||
|
"name": aliasName,
|
||||||
|
"canonical_id": entityID,
|
||||||
|
"mount_accessor": mountAccessor,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create an entity alias:%v", err)
|
||||||
|
}
|
||||||
|
aliasID := aliasResp.Data["id"].(string)
|
||||||
|
if aliasID == "" {
|
||||||
|
t.Fatal("Alias ID not present in response")
|
||||||
|
}
|
||||||
|
_, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("auth/userpass/users/%s", aliasName), map[string]interface{}{
|
||||||
|
"password": "testpassword",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to configure userpass backend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userClient, entityID, aliasID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTOTPMount enables the totp secrets engine by mounting it. This requires
|
||||||
|
// that the test cluster has a totp backend available.
|
||||||
|
func SetupTOTPMount(t testing.T, client *api.Client) {
|
||||||
|
t.Helper()
|
||||||
|
// Mount the TOTP backend
|
||||||
|
mountInfo := &api.MountInput{
|
||||||
|
Type: "totp",
|
||||||
|
}
|
||||||
|
if err := client.Sys().Mount("totp", mountInfo); err != nil {
|
||||||
|
t.Fatalf("failed to mount totp backend: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTOTPMethod configures the TOTP secrets engine with a provided config map.
|
||||||
|
func SetupTOTPMethod(t testing.T, client *api.Client, config map[string]interface{}) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
resp1, err := client.Logical().Write("identity/mfa/method/totp", config)
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupMFALoginEnforcement configures a single enforcement method using the
|
||||||
|
// provided config map. "name" field is required in the config map.
|
||||||
|
func SetupMFALoginEnforcement(t testing.T, client *api.Client, config map[string]interface{}) {
|
||||||
|
t.Helper()
|
||||||
|
enfName, ok := config["name"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("couldn't find name in login-enforcement config")
|
||||||
|
}
|
||||||
|
_, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/login-enforcement/%s", enfName), config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to configure MFAEnforcementConfig: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupUserpassMountAccessor sets up userpass auth and returns its mount
|
||||||
|
// accessor. This requires that the test cluster has a "userpass" auth method
|
||||||
|
// available.
|
||||||
|
func SetupUserpassMountAccessor(t testing.T, client *api.Client) string {
|
||||||
|
t.Helper()
|
||||||
|
var mountAccessor string
|
||||||
|
// 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().ListAuthWithContext(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to list auth methods: %v", err)
|
||||||
|
}
|
||||||
|
if auths != nil && auths["userpass/"] != nil {
|
||||||
|
mountAccessor = auths["userpass/"].Accessor
|
||||||
|
} else {
|
||||||
|
t.Fatalf("failed to get userpass mount accessor")
|
||||||
|
}
|
||||||
|
|
||||||
|
return mountAccessor
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterEntityInTOTPEngine registers an entity with a methodID and returns
|
||||||
|
// the generated name.
|
||||||
|
func RegisterEntityInTOTPEngine(t testing.T, client *api.Client, entityID, methodID string) string {
|
||||||
|
t.Helper()
|
||||||
|
totpGenName := fmt.Sprintf("%s-%s", entityID, methodID)
|
||||||
|
secret, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-generate"), map[string]interface{}{
|
||||||
|
"entity_id": entityID,
|
||||||
|
"method_id": methodID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate a TOTP secret on an entity: %v", err)
|
||||||
|
}
|
||||||
|
totpURL := secret.Data["url"].(string)
|
||||||
|
if totpURL == "" {
|
||||||
|
t.Fatalf("failed to get TOTP url in secret response: %+v", secret)
|
||||||
|
}
|
||||||
|
_, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("totp/keys/%s", totpGenName), map[string]interface{}{
|
||||||
|
"url": totpURL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to register a TOTP URL: %v", err)
|
||||||
|
}
|
||||||
|
_, err = client.Logical().WriteWithContext(context.Background(), "identity/mfa/login-enforcement/randomName", map[string]interface{}{
|
||||||
|
"name": "randomName",
|
||||||
|
"identity_entity_ids": []string{entityID},
|
||||||
|
"mfa_method_ids": []string{methodID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create login enforcement")
|
||||||
|
}
|
||||||
|
|
||||||
|
return totpGenName
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTOTPCodeFromEngine requests a TOTP code from the specified enginePath.
|
||||||
|
func GetTOTPCodeFromEngine(t testing.T, client *api.Client, enginePath string) string {
|
||||||
|
t.Helper()
|
||||||
|
totpPath := fmt.Sprintf("totp/code/%s", enginePath)
|
||||||
|
secret, err := client.Logical().ReadWithContext(context.Background(), totpPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create totp passcode: %v", err)
|
||||||
|
}
|
||||||
|
if secret == nil {
|
||||||
|
t.Fatalf("bad secret returned from %s", totpPath)
|
||||||
|
}
|
||||||
|
return secret.Data["code"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupLoginMFATOTP setups up a TOTP MFA using some basic configuration and
|
||||||
|
// returns all relevant information to the client.
|
||||||
|
func SetupLoginMFATOTP(t testing.T, client *api.Client) (*api.Client, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
// Mount the totp secrets engine
|
||||||
|
SetupTOTPMount(t, client)
|
||||||
|
|
||||||
|
// Create a mount accessor to associate with an entity
|
||||||
|
mountAccessor := SetupUserpassMountAccessor(t, client)
|
||||||
|
|
||||||
|
// Create a test entity and alias
|
||||||
|
entityClient, entityID, _ := CreateEntityAndAlias(t, client, mountAccessor, "entity1", "testuser1")
|
||||||
|
|
||||||
|
// Configure a default TOTP method
|
||||||
|
totpConfig := map[string]interface{}{
|
||||||
|
"issuer": "yCorp",
|
||||||
|
"period": 5,
|
||||||
|
"algorithm": "SHA256",
|
||||||
|
"digits": 6,
|
||||||
|
"skew": 0,
|
||||||
|
"key_size": 20,
|
||||||
|
"qr_size": 200,
|
||||||
|
"max_validation_attempts": 5,
|
||||||
|
}
|
||||||
|
methodID := SetupTOTPMethod(t, client, totpConfig)
|
||||||
|
|
||||||
|
// Configure a default login enforcement
|
||||||
|
enforcementConfig := map[string]interface{}{
|
||||||
|
"auth_method_types": []string{"userpass"},
|
||||||
|
"name": "randomName",
|
||||||
|
"mfa_method_ids": []string{methodID},
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupMFALoginEnforcement(t, client, enforcementConfig)
|
||||||
|
return entityClient, entityID, methodID
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
auth "github.com/hashicorp/vault/api/auth/userpass"
|
auth "github.com/hashicorp/vault/api/auth/userpass"
|
||||||
"github.com/hashicorp/vault/builtin/credential/github"
|
"github.com/hashicorp/vault/builtin/credential/github"
|
||||||
"github.com/hashicorp/vault/builtin/credential/userpass"
|
"github.com/hashicorp/vault/builtin/credential/userpass"
|
||||||
|
"github.com/hashicorp/vault/helper/testhelpers"
|
||||||
vaulthttp "github.com/hashicorp/vault/http"
|
vaulthttp "github.com/hashicorp/vault/http"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
@@ -301,7 +302,7 @@ func TestIdentityStore_MergeEntities_FailsDueToClash(t *testing.T) {
|
|||||||
t.Fatal("did not find userpass accessor")
|
t.Fatal("did not find userpass accessor")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdBob, aliasIdBob := createEntityAndAlias(client, mountAccessor, "bob-smith", "bob", t)
|
_, entityIdBob, aliasIdBob := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "bob-smith", "bob")
|
||||||
|
|
||||||
// Create userpass login for alice
|
// Create userpass login for alice
|
||||||
_, err = client.Logical().Write("auth/userpass/users/alice", map[string]interface{}{
|
_, err = client.Logical().Write("auth/userpass/users/alice", map[string]interface{}{
|
||||||
@@ -311,7 +312,7 @@ func TestIdentityStore_MergeEntities_FailsDueToClash(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdAlice, aliasIdAlice := createEntityAndAlias(client, mountAccessor, "alice-smith", "alice", t)
|
_, entityIdAlice, aliasIdAlice := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "alice-smith", "alice")
|
||||||
|
|
||||||
// Perform entity merge
|
// Perform entity merge
|
||||||
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
||||||
@@ -404,9 +405,9 @@ func TestIdentityStore_MergeEntities_FailsDueToClashInFromEntities(t *testing.T)
|
|||||||
t.Fatal("did not find github accessor")
|
t.Fatal("did not find github accessor")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdBob, _ := createEntityAndAlias(client, mountAccessor, "bob-smith", "bob", t)
|
_, entityIdBob, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "bob-smith", "bob")
|
||||||
_, entityIdAlice, _ := createEntityAndAlias(client, mountAccessorGitHub, "alice-smith", "alice", t)
|
_, entityIdAlice, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessorGitHub, "alice-smith", "alice")
|
||||||
_, entityIdClara, _ := createEntityAndAlias(client, mountAccessorGitHub, "clara-smith", "clara", t)
|
_, entityIdClara, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessorGitHub, "clara-smith", "clara")
|
||||||
|
|
||||||
// Perform entity merge
|
// Perform entity merge
|
||||||
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
||||||
@@ -491,7 +492,7 @@ func TestIdentityStore_MergeEntities_FailsDueToDoubleClash(t *testing.T) {
|
|||||||
t.Fatal("did not find github accessor")
|
t.Fatal("did not find github accessor")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdBob, aliasIdBob := createEntityAndAlias(client, mountAccessor, "bob-smith", "bob", t)
|
_, entityIdBob, aliasIdBob := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "bob-smith", "bob")
|
||||||
|
|
||||||
aliasResp, err := client.Logical().Write("identity/entity-alias", map[string]interface{}{
|
aliasResp, err := client.Logical().Write("identity/entity-alias", map[string]interface{}{
|
||||||
"name": "bob-github",
|
"name": "bob-github",
|
||||||
@@ -515,8 +516,8 @@ func TestIdentityStore_MergeEntities_FailsDueToDoubleClash(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdAlice, aliasIdAlice := createEntityAndAlias(client, mountAccessor, "alice-smith", "alice", t)
|
_, entityIdAlice, aliasIdAlice := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "alice-smith", "alice")
|
||||||
_, entityIdClara, aliasIdClara := createEntityAndAlias(client, mountAccessorGitHub, "clara-smith", "clara", t)
|
_, entityIdClara, aliasIdClara := testhelpers.CreateEntityAndAlias(t, client, mountAccessorGitHub, "clara-smith", "clara")
|
||||||
|
|
||||||
// Perform entity merge
|
// Perform entity merge
|
||||||
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
||||||
@@ -602,7 +603,7 @@ func TestIdentityStore_MergeEntities_FailsDueToClashInFromEntities_CheckRawReque
|
|||||||
t.Fatal("did not find userpass accessor")
|
t.Fatal("did not find userpass accessor")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdBob, _ := createEntityAndAlias(client, mountAccessor, "bob-smith", "bob", t)
|
_, entityIdBob, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "bob-smith", "bob")
|
||||||
|
|
||||||
// Create userpass login for alice
|
// Create userpass login for alice
|
||||||
_, err = client.Logical().Write("auth/userpass/users/alice", map[string]interface{}{
|
_, err = client.Logical().Write("auth/userpass/users/alice", map[string]interface{}{
|
||||||
@@ -612,7 +613,7 @@ func TestIdentityStore_MergeEntities_FailsDueToClashInFromEntities_CheckRawReque
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdAlice, _ := createEntityAndAlias(client, mountAccessor, "alice-smith", "alice", t)
|
_, entityIdAlice, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "alice-smith", "alice")
|
||||||
|
|
||||||
// Perform entity merge as a Raw Request so we can investigate the response body
|
// Perform entity merge as a Raw Request so we can investigate the response body
|
||||||
req := client.NewRequest("POST", "/v1/identity/entity/merge")
|
req := client.NewRequest("POST", "/v1/identity/entity/merge")
|
||||||
@@ -772,7 +773,7 @@ func TestIdentityStore_MergeEntities_SameMountAccessor_ThenUseAlias(t *testing.T
|
|||||||
t.Fatal("did not find userpass accessor")
|
t.Fatal("did not find userpass accessor")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdBob, aliasIdBob := createEntityAndAlias(client, mountAccessor, "bob-smith", "bob", t)
|
_, entityIdBob, aliasIdBob := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "bob-smith", "bob")
|
||||||
|
|
||||||
// Create userpass login for alice
|
// Create userpass login for alice
|
||||||
_, err = client.Logical().Write("auth/userpass/users/alice", map[string]interface{}{
|
_, err = client.Logical().Write("auth/userpass/users/alice", map[string]interface{}{
|
||||||
@@ -788,7 +789,7 @@ func TestIdentityStore_MergeEntities_SameMountAccessor_ThenUseAlias(t *testing.T
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdAlice, _ := createEntityAndAlias(client, mountAccessor, "alice-smith", "alice", t)
|
_, entityIdAlice, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "alice-smith", "alice")
|
||||||
|
|
||||||
// Try and login with alias 2 (alice) pre-merge
|
// Try and login with alias 2 (alice) pre-merge
|
||||||
userpassAuth, err := auth.NewUserpassAuth("alice", &auth.Password{FromString: "testpassword"})
|
userpassAuth, err := auth.NewUserpassAuth("alice", &auth.Password{FromString: "testpassword"})
|
||||||
@@ -909,7 +910,7 @@ func TestIdentityStore_MergeEntities_FailsDueToMultipleClashMergesAttempted(t *t
|
|||||||
t.Fatal("did not find github accessor")
|
t.Fatal("did not find github accessor")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdBob, _ := createEntityAndAlias(client, mountAccessor, "bob-smith", "bob", t)
|
_, entityIdBob, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "bob-smith", "bob")
|
||||||
aliasResp, err := client.Logical().Write("identity/entity-alias", map[string]interface{}{
|
aliasResp, err := client.Logical().Write("identity/entity-alias", map[string]interface{}{
|
||||||
"name": "bob-github",
|
"name": "bob-github",
|
||||||
"canonical_id": entityIdBob,
|
"canonical_id": entityIdBob,
|
||||||
@@ -932,8 +933,8 @@ func TestIdentityStore_MergeEntities_FailsDueToMultipleClashMergesAttempted(t *t
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, entityIdAlice, aliasIdAlice := createEntityAndAlias(client, mountAccessor, "alice-smith", "alice", t)
|
_, entityIdAlice, aliasIdAlice := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "alice-smith", "alice")
|
||||||
_, entityIdClara, aliasIdClara := createEntityAndAlias(client, mountAccessorGitHub, "clara-smith", "alice", t)
|
_, entityIdClara, aliasIdClara := testhelpers.CreateEntityAndAlias(t, client, mountAccessorGitHub, "clara-smith", "alice")
|
||||||
|
|
||||||
// Perform entity merge
|
// Perform entity merge
|
||||||
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
mergeResp, err := client.Logical().Write("identity/entity/merge", map[string]interface{}{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
upAuth "github.com/hashicorp/vault/api/auth/userpass"
|
upAuth "github.com/hashicorp/vault/api/auth/userpass"
|
||||||
|
"github.com/hashicorp/vault/helper/testhelpers"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/api"
|
"github.com/hashicorp/vault/api"
|
||||||
"github.com/hashicorp/vault/audit"
|
"github.com/hashicorp/vault/audit"
|
||||||
@@ -18,70 +19,9 @@ import (
|
|||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createEntityAndAlias(client *api.Client, mountAccessor, entityName, aliasName string, t *testing.T) (*api.Client, string, string) {
|
func doTwoPhaseLogin(t *testing.T, client *api.Client, totpCodePath, methodID, username string) {
|
||||||
_, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("auth/userpass/users/%s", aliasName), map[string]interface{}{
|
t.Helper()
|
||||||
"password": "testpassword",
|
totpPasscode := testhelpers.GetTOTPCodeFromEngine(t, client, totpCodePath)
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to configure userpass backend: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
userClient, err := client.Clone()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to clone the client:%v", err)
|
|
||||||
}
|
|
||||||
userClient.SetToken(client.Token())
|
|
||||||
|
|
||||||
resp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity", map[string]interface{}{
|
|
||||||
"name": entityName,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create an entity:%v", err)
|
|
||||||
}
|
|
||||||
entityID := resp.Data["id"].(string)
|
|
||||||
|
|
||||||
aliasResp, err := client.Logical().WriteWithContext(context.Background(), "identity/entity-alias", map[string]interface{}{
|
|
||||||
"name": aliasName,
|
|
||||||
"canonical_id": entityID,
|
|
||||||
"mount_accessor": mountAccessor,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create an entity alias:%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasID := aliasResp.Data["id"].(string)
|
|
||||||
if aliasID == "" {
|
|
||||||
t.Fatal("Alias ID not present in response")
|
|
||||||
}
|
|
||||||
return userClient, entityID, aliasID
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerEntityInTOTPEngine(client *api.Client, entityID, methodID string, t *testing.T) string {
|
|
||||||
totpGenName := fmt.Sprintf("%s-%s", entityID, methodID)
|
|
||||||
secret, err := client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-generate"), map[string]interface{}{
|
|
||||||
"entity_id": entityID,
|
|
||||||
"method_id": methodID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to generate a TOTP secret on an entity: %v", err)
|
|
||||||
}
|
|
||||||
totpURL := secret.Data["url"].(string)
|
|
||||||
|
|
||||||
_, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("totp/keys/%s", totpGenName), map[string]interface{}{
|
|
||||||
"url": totpURL,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to register a TOTP URL: %v", err)
|
|
||||||
}
|
|
||||||
return totpGenName
|
|
||||||
}
|
|
||||||
|
|
||||||
func doTwoPhaseLogin(client *api.Client, totpCodePath, methodID, username string, t *testing.T) {
|
|
||||||
totpResp, err := client.Logical().ReadWithContext(context.Background(), totpCodePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create totp passcode: %v", err)
|
|
||||||
}
|
|
||||||
totpPasscode := totpResp.Data["code"].(string)
|
|
||||||
|
|
||||||
upMethod, err := upAuth.NewUserpassAuth(username, &upAuth.Password{FromString: "testpassword"})
|
upMethod, err := upAuth.NewUserpassAuth(username, &upAuth.Password{FromString: "testpassword"})
|
||||||
|
|
||||||
@@ -135,91 +75,48 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
|
|||||||
client := cluster.Cores[0].Client
|
client := cluster.Cores[0].Client
|
||||||
|
|
||||||
// Enable the audit backend
|
// Enable the audit backend
|
||||||
err := client.Sys().EnableAuditWithOptions("noop", &api.EnableAuditOptions{Type: "noop"})
|
if err := client.Sys().EnableAuditWithOptions("noop", &api.EnableAuditOptions{Type: "noop"}); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mount the TOTP backend
|
testhelpers.SetupTOTPMount(t, client)
|
||||||
mountInfo := &api.MountInput{
|
mountAccessor := testhelpers.SetupUserpassMountAccessor(t, client)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
auths, err := client.Sys().ListAuthWithContext(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bb")
|
|
||||||
}
|
|
||||||
var mountAccessor string
|
|
||||||
if auths != nil && auths["userpass/"] != nil {
|
|
||||||
mountAccessor = auths["userpass/"].Accessor
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating two users in the userpass auth mount
|
// Creating two users in the userpass auth mount
|
||||||
userClient1, entityID1, _ := createEntityAndAlias(client, mountAccessor, "entity1", "testuser1", t)
|
userClient1, entityID1, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "entity1", "testuser1")
|
||||||
userClient2, entityID2, _ := createEntityAndAlias(client, mountAccessor, "entity2", "testuser2", t)
|
userClient2, entityID2, _ := testhelpers.CreateEntityAndAlias(t, client, mountAccessor, "entity2", "testuser2")
|
||||||
|
|
||||||
// configure TOTP secret engine
|
totpConfig := map[string]interface{}{
|
||||||
var methodID string
|
"issuer": "yCorp",
|
||||||
// login MFA
|
"period": 10,
|
||||||
{
|
"algorithm": "SHA512",
|
||||||
// create a config
|
"digits": 6,
|
||||||
resp1, err := client.Logical().Write("identity/mfa/method/totp", map[string]interface{}{
|
"skew": 0,
|
||||||
"issuer": "yCorp",
|
"key_size": 20,
|
||||||
"period": 5,
|
"qr_size": 200,
|
||||||
"algorithm": "SHA1",
|
"max_validation_attempts": 5,
|
||||||
"digits": 6,
|
|
||||||
"skew": 1,
|
|
||||||
"key_size": 10,
|
|
||||||
"qr_size": 100,
|
|
||||||
"max_validation_attempts": 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
// creating MFAEnforcementConfig
|
|
||||||
_, err = client.Logical().WriteWithContext(context.Background(), "identity/mfa/login-enforcement/randomName", map[string]interface{}{
|
|
||||||
"auth_method_types": []string{"userpass"},
|
|
||||||
"name": "randomName",
|
|
||||||
"mfa_method_ids": []string{methodID},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to configure MFAEnforcementConfig: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
methodID := testhelpers.SetupTOTPMethod(t, client, totpConfig)
|
||||||
|
|
||||||
// registering EntityIDs in the TOTP secret Engine for MethodID
|
// registering EntityIDs in the TOTP secret Engine for MethodID
|
||||||
totpEngineConfigName1 := registerEntityInTOTPEngine(client, entityID1, methodID, t)
|
enginePath1 := testhelpers.RegisterEntityInTOTPEngine(t, client, entityID1, methodID)
|
||||||
totpEngineConfigName2 := registerEntityInTOTPEngine(client, entityID2, methodID, t)
|
enginePath2 := testhelpers.RegisterEntityInTOTPEngine(t, client, entityID2, methodID)
|
||||||
|
|
||||||
|
// Configure a default login enforcement
|
||||||
|
enforcementConfig := map[string]interface{}{
|
||||||
|
"auth_method_types": []string{"userpass"},
|
||||||
|
"name": "randomName",
|
||||||
|
"mfa_method_ids": []string{methodID},
|
||||||
|
}
|
||||||
|
|
||||||
|
testhelpers.SetupMFALoginEnforcement(t, client, enforcementConfig)
|
||||||
|
|
||||||
// MFA single-phase login
|
// MFA single-phase login
|
||||||
totpCodePath1 := fmt.Sprintf("totp/code/%s", totpEngineConfigName1)
|
totpPasscode1 := testhelpers.GetTOTPCodeFromEngine(t, client, enginePath1)
|
||||||
secret, err := client.Logical().ReadWithContext(context.Background(), totpCodePath1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create totp passcode: %v", err)
|
|
||||||
}
|
|
||||||
totpPasscode1 := secret.Data["code"].(string)
|
|
||||||
|
|
||||||
userClient1.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", methodID, totpPasscode1))
|
userClient1.AddHeader("X-Vault-MFA", fmt.Sprintf("%s:%s", methodID, totpPasscode1))
|
||||||
secret, err = userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{
|
secret, err := userClient1.Logical().WriteWithContext(context.Background(), "auth/userpass/login/testuser1", map[string]interface{}{
|
||||||
"password": "testpassword",
|
"password": "testpassword",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -280,13 +177,8 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
|
|||||||
|
|
||||||
// validation
|
// validation
|
||||||
// waiting for 5 seconds so that a fresh code could be generated
|
// waiting for 5 seconds so that a fresh code could be generated
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
// getting a fresh totp passcode for the validation step
|
totpPasscode1 = testhelpers.GetTOTPCodeFromEngine(t, client, enginePath1)
|
||||||
totpResp, err := client.Logical().ReadWithContext(context.Background(), totpCodePath1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create totp passcode: %v", err)
|
|
||||||
}
|
|
||||||
totpPasscode1 = totpResp.Data["code"].(string)
|
|
||||||
|
|
||||||
secret, err = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{
|
secret, err = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{
|
||||||
"mfa_request_id": secret.Auth.MFARequirement.MFARequestID,
|
"mfa_request_id": secret.Auth.MFARequirement.MFARequestID,
|
||||||
@@ -350,7 +242,9 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var maxErr error
|
var maxErr error
|
||||||
for i := 0; i < 4; i++ {
|
maxAttempts := 6
|
||||||
|
i := 0
|
||||||
|
for i = 0; i < maxAttempts; i++ {
|
||||||
_, maxErr = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{
|
_, maxErr = userClient1.Logical().WriteWithContext(context.Background(), "sys/mfa/validate", map[string]interface{}{
|
||||||
"mfa_request_id": secret.Auth.MFARequirement.MFARequestID,
|
"mfa_request_id": secret.Auth.MFARequirement.MFARequestID,
|
||||||
"mfa_payload": map[string][]string{
|
"mfa_payload": map[string][]string{
|
||||||
@@ -361,18 +255,16 @@ func TestLoginMfaGenerateTOTPTestAuditIncluded(t *testing.T) {
|
|||||||
t.Fatalf("MFA succeeded with an invalid passcode")
|
t.Fatalf("MFA succeeded with an invalid passcode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts 4 exceeded the allowed attempts 3") {
|
if !strings.Contains(maxErr.Error(), "maximum TOTP validation attempts") {
|
||||||
t.Fatalf("unexpected error message when exceeding max failed validation attempts")
|
t.Fatalf("unexpected error message when exceeding max failed validation attempts: %s", maxErr.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// let's make sure the configID is not blocked for other users
|
// let's make sure the configID is not blocked for other users
|
||||||
totpCodePath2 := fmt.Sprintf("totp/code/%s", totpEngineConfigName2)
|
doTwoPhaseLogin(t, userClient2, enginePath2, methodID, "testuser2")
|
||||||
doTwoPhaseLogin(userClient2, totpCodePath2, methodID, "testuser2", t)
|
|
||||||
|
|
||||||
// let's see if user1 is able to login after 5 seconds
|
// let's see if user1 is able to login after 5 seconds
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
// getting a fresh totp passcode for the validation step
|
doTwoPhaseLogin(t, userClient1, enginePath1, methodID, "testuser1")
|
||||||
doTwoPhaseLogin(userClient1, totpCodePath1, methodID, "testuser1", t)
|
|
||||||
|
|
||||||
// Destroy the secret so that the token can self generate
|
// Destroy the secret so that the token can self generate
|
||||||
_, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{
|
_, err = client.Logical().WriteWithContext(context.Background(), fmt.Sprintf("identity/mfa/method/totp/admin-destroy"), map[string]interface{}{
|
||||||
|
|||||||
Reference in New Issue
Block a user