-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
interactive CLI for mfa login #14131
Conversation
* 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
* preventing replay attack on MFA passcodes * using %w instead of %s for error
CLI prints a warning message indicating the login request needs to get validated
command/write.go
Outdated
"mfa_payload": mfaPayload, | ||
} | ||
path = "sys/mfa/validate" | ||
goto WRITE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather see the write code repeated than have this goto.
command/write.go
Outdated
@@ -150,11 +155,97 @@ func (c *WriteCommand) Run(args []string) int { | |||
c.UI.Warn(wrapAtLength("A login request was issued that is subject to "+ | |||
"MFA validation. Please make sure to validate the login by sending another "+ | |||
"request to mfa/validate endpoint.") + "\n") | |||
ok := YesNoPrompt("Would you like to interactively validate MFA methods?", true) | |||
if ok { | |||
mfaPayload := make(map[string][]string, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you extract this into a func so we can use return instead of goto?
command/base.go
Outdated
Name: "non-interactive", | ||
Target: &c.flagNonInteractive, | ||
Default: false, | ||
Usage: "It controls a command to be executed in an interactive or non-interactive fashion with a user.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usage: "It controls a command to be executed in an interactive or non-interactive fashion with a user.", | |
Usage: "When set true, prevents asking the user for input via the terminal.", |
Default: false, | ||
Usage: "It controls a command to be executed in an interactive or non-interactive fashion with a user.", | ||
}) | ||
|
||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also use github.com/mattn/go-isatty to override the var to true when we can't ask the user for input (because there's no tty)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, ui.AskSecret already takes care of that here:
https://github.com/mitchellh/cli/blob/5454ffe87bc5c6d8b6b21c825617755e18a07828/ui.go#L79
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is less desirable, because then if the user didn't say -non-interactive, but they're running without a terminal, we'll simply block waiting for input on stdin. This will be a surprising change and it won't be obvious what the problem is.
command/write.go
Outdated
var passcode string | ||
var err error | ||
if usePasscode { | ||
passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q:", mfaMethodID)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if we could tell them the method type, not sure if we have that info.
command/write.go
Outdated
if usePasscode { | ||
passcode, err = c.UI.AskSecret(fmt.Sprintf("Enter the passphrase for methodID %q:", mfaMethodID)) | ||
if err != nil { | ||
c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to mfa/validate", err.Error())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to mfa/validate", err.Error())) | |
c.UI.Error(fmt.Sprintf("failed to read the passphrase with error %q. please validate the login by sending a request to sys/mfa/validate", err.Error())) |
command/write.go
Outdated
} | ||
} else { | ||
// TODO: not sure about printing this message; an error might occur before or during the validate request | ||
c.UI.Warn("Please acknowledge the push notification in your authenticator app") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't we prompt them (e.g. "hit enter") so we know when they've done it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, at this point, the validate request has not issued yet. This is just a reminder for them to check their authenticator app. There is a chance that the validate request fails even before issuing the notification.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Maybe we can make the message a little more explicit then, something like "Asking Vault to perform MFA validation with upstream service. You should receive a push notification in your authenticator app shortly"? Otherwise if they go and look immediately they might not see the push yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me, modulo Nick's outstanding comment(s).
…nd non-interactive. A user without a terminal should not be able to use the interactive mode.
I tested the following combinations:
|
@@ -254,6 +272,24 @@ func TestLoginMfaGenerateTOTPRoleTest(t *testing.T) { | |||
t.Fatalf("MFA failed: %v", err) | |||
} | |||
|
|||
if secret.Auth == nil || secret.Auth.ClientToken == "" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused by these test changes - are they related to the new rest of the PR? It looks like we're just checking in the audit log to see if the explicit write we did to sys/mfa/validate resulted in an audit record.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, I just did not want to push another PR for it. I have made a note in the PR description about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
This PR is a step to improve CLI for mfa login. For a login request that is subject to MFA validation, it interactively asks user for credentials to validate the request, and return the client token.
Also, enabled audit backend in a test to make sure mfa/validate is audited.
Example of the CLI output in interactive mode: