Skip to content

Commit

Permalink
Add --mfa-mode=sso support; Add cli prompt UX changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
Joerger committed Oct 25, 2024
1 parent 5848054 commit 9b4ce69
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 31 deletions.
3 changes: 3 additions & 0 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,9 @@ type Config struct {
// authenticators, such as remote hosts or virtual machines.
PreferOTP bool

// PreferSSO prefers SSO in favor of other MFA methods.
PreferSSO bool

// CheckVersions will check that client version is compatible
// with auth server version when connecting.
CheckVersions bool
Expand Down
1 change: 1 addition & 0 deletions lib/client/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func (tc *TeleportClient) NewMFAPrompt(opts ...mfa.PromptOpt) mfa.Prompt {
PromptConfig: *cfg,
Writer: tc.Stderr,
PreferOTP: tc.PreferOTP,
PreferSSO: tc.PreferSSO,
AllowStdinHijack: tc.AllowStdinHijack,
StdinFunc: tc.StdinFunc,
})
Expand Down
73 changes: 62 additions & 11 deletions lib/client/mfa/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"log/slog"
"os"
"runtime"
"strings"
"sync"

"github.com/gravitational/trace"
Expand All @@ -37,6 +38,15 @@ import (
"github.com/gravitational/teleport/lib/auth/webauthnwin"
)

const (
// cliMFATypeOTP is the CLI display name for OTP.
cliMFATypeOTP = "OTP"
// cliMFATypeWebauthn is the CLI display name for Webauthn.
cliMFATypeWebauthn = "WEBAUTHN"
// cliMFATypeSSO is the CLI display name for SSO.
cliMFATypeSSO = "SSO"
)

// CLIPromptConfig contains CLI prompt config options.
type CLIPromptConfig struct {
PromptConfig
Expand All @@ -51,6 +61,9 @@ type CLIPromptConfig struct {
// PreferOTP favors OTP challenges, if applicable.
// Takes precedence over AuthenticatorAttachment settings.
PreferOTP bool
// PreferSSO favors SSO challenges, if applicable.
// Takes precedence over AuthenticatorAttachment settings.
PreferSSO bool
// StdinFunc allows tests to override prompt.Stdin().
// If nil prompt.Stdin() is used.
StdinFunc func() prompt.StdinReader
Expand Down Expand Up @@ -112,17 +125,25 @@ func (c *CLIPrompt) Run(ctx context.Context, chal *proto.MFAAuthenticateChalleng
promptSSO := chal.SSOChallenge != nil

// No prompt to run, no-op.
if !promptOTP && !promptWebauthn {
if !promptOTP && !promptWebauthn && !promptSSO {
return &proto.MFAAuthenticateResponse{}, nil
}

var availableMethods []string
if promptWebauthn {
availableMethods = append(availableMethods, cliMFATypeWebauthn)
}
if promptSSO {
availableMethods = append(availableMethods, cliMFATypeSSO)
}
if promptOTP {
availableMethods = append(availableMethods, cliMFATypeOTP)
}

// Check off unsupported methods.
if promptWebauthn && !c.cfg.WebauthnSupported {
promptWebauthn = false
slog.DebugContext(ctx, "hardware device MFA not supported by your platform")
if !promptOTP {
return nil, trace.BadParameter("hardware device MFA not supported by your platform, please register an OTP device")
}
}

if promptSSO && c.cfg.SSOMFACeremony == nil {
Expand All @@ -131,19 +152,50 @@ func (c *CLIPrompt) Run(ctx context.Context, chal *proto.MFAAuthenticateChalleng
}

// Prefer whatever method is requested by the client.
if c.cfg.PreferOTP && promptOTP {
promptWebauthn = false
var chosenMethods []string
var userSpecifiedMethod bool
switch {
case c.cfg.PreferSSO && promptSSO:
chosenMethods = []string{cliMFATypeSSO}
promptWebauthn, promptOTP = false, false
userSpecifiedMethod = true
case c.cfg.PreferOTP && promptOTP:
chosenMethods = []string{cliMFATypeOTP}
promptWebauthn, promptSSO = false, false
userSpecifiedMethod = true
}

// Use stronger auth methods if hijack is not allowed.
if !c.cfg.AllowStdinHijack && promptWebauthn {
promptOTP = false
}

// If a specific webauthn attachment was requested, skip OTP.
// Otherwise, allow dual prompt with OTP.
if promptWebauthn && c.cfg.AuthenticatorAttachment != wancli.AttachmentAuto {
// If we have multiple viable options, prefer Webauthn > SSO > OTP.
switch {
case promptWebauthn:
chosenMethods = []string{cliMFATypeWebauthn}
promptSSO = false

// If a specific webauthn attachment was requested, skip OTP.
// Otherwise, allow dual prompt with OTP.
promptOTP = promptOTP && c.cfg.AuthenticatorAttachment == wancli.AttachmentAuto
if promptOTP {
chosenMethods = append(chosenMethods, cliMFATypeOTP)
}
case promptSSO:
chosenMethods = []string{cliMFATypeSSO}
promptOTP = false
case promptOTP:
chosenMethods = []string{cliMFATypeOTP}
}

// If there are multiple options and we chose one without it being specifically
// requested by the user, notify the user about it and how to request a specific method.
if len(availableMethods) > len(chosenMethods) && len(chosenMethods) > 0 && !userSpecifiedMethod {
const msg = "" +
"Available MFA methods [%v]. Continuing with %v.\n" +
"If you wish to perform MFA with another method, specify with flag --mfa-mode=<sso,otp>.\n\n"
fmt.Fprintf(c.writer(), msg, strings.Join(availableMethods, ", "), strings.Join(chosenMethods, " and "))
}

switch {
Expand All @@ -160,8 +212,7 @@ func (c *CLIPrompt) Run(ctx context.Context, chal *proto.MFAAuthenticateChalleng
resp, err := c.promptOTP(ctx, c.cfg.Quiet)
return resp, trace.Wrap(err)
default:
// We shouldn't reach this case as we would have hit the no-op case above.
return nil, trace.BadParameter("no MFA methods to prompt")
return nil, trace.BadParameter("client does not support any available MFA methods [%v], see debug logs for details", strings.Join(availableMethods, ", "))
}
}

Expand Down
Loading

0 comments on commit 9b4ce69

Please sign in to comment.