diff --git a/lib/client/api.go b/lib/client/api.go index 0a7b35dbd5e51..e321aec2bec8a 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -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 diff --git a/lib/client/mfa.go b/lib/client/mfa.go index f03dc69941e87..98ef5657bc007 100644 --- a/lib/client/mfa.go +++ b/lib/client/mfa.go @@ -62,6 +62,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, }, diff --git a/lib/client/mfa/cli.go b/lib/client/mfa/cli.go index d9d22e258ddd5..7a2911f78a01a 100644 --- a/lib/client/mfa/cli.go +++ b/lib/client/mfa/cli.go @@ -25,6 +25,7 @@ import ( "log/slog" "os" "runtime" + "strings" "sync" "github.com/gravitational/trace" @@ -42,6 +43,8 @@ const ( 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. @@ -58,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 @@ -101,40 +107,71 @@ func (c *CLIPrompt) Run(ctx context.Context, chal *proto.MFAAuthenticateChalleng promptOTP := chal.TOTP != nil promptWebauthn := chal.WebauthnChallenge != nil + promptSSO := false // TODO(Joerger): check for SSO challenge once added in separate PR. // 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.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") - } } // Prefer whatever method is requested by the client. + var chosenMethods []string switch { + case c.PreferSSO && promptSSO: + chosenMethods = []string{cliMFATypeSSO} + promptWebauthn, promptOTP = false, false case c.PreferOTP && promptOTP: - promptWebauthn = false + chosenMethods = []string{cliMFATypeOTP} + promptWebauthn, promptSSO = false, false } // Use stronger auth methods if hijack is not allowed. - if !c.AllowStdinHijack && promptWebauthn { + if !c.AllowStdinHijack && (promptWebauthn || promptSSO) { promptOTP = false } - // If we have multiple viable options, prefer Webauthn > OTP. + // 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. if c.AuthenticatorAttachment != wancli.AttachmentAuto { promptOTP = false + } else if promptOTP { + chosenMethods = append(chosenMethods, cliMFATypeOTP) } + case promptSSO: + chosenMethods = []string{cliMFATypeSSO} + promptOTP = false + case promptOTP: + chosenMethods = []string{cliMFATypeOTP} + } + + if len(chosenMethods) > 0 { + const msg = "" + + "Available MFA methods [%v]. Continuing with %v.\n" + + "If you wish to perform MFA with another method, specify with flag --mfa-mode=.\n\n" + fmt.Fprintf(c.Writer, msg, strings.Join(availableMethods, ", "), strings.Join(chosenMethods, "and ")) } switch { @@ -144,12 +181,14 @@ func (c *CLIPrompt) Run(ctx context.Context, chal *proto.MFAAuthenticateChalleng case promptWebauthn: resp, err := c.promptWebauthn(ctx, chal, c.getWebauthnPrompt(ctx)) return resp, trace.Wrap(err) + case promptSSO: + // TODO(Joerger): prompt for SSO once implemented. + return nil, trace.NotImplemented("SSO MFA not implemented") case promptOTP: resp, err := c.promptOTP(ctx, c.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]", strings.Join(availableMethods, ", ")) } } diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index d7573c092a9d2..1375e455af9b4 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -117,6 +117,8 @@ const ( mfaModePlatform = "platform" // mfaModeOTP utilizes only OTP devices. mfaModeOTP = "otp" + // mfaModeSSO utilizes only SSO devices. + mfaModeSSO = "sso" ) const ( @@ -756,7 +758,7 @@ func Run(ctx context.Context, args []string, opts ...CliOption) error { app.Flag("bind-addr", "Override host:port used when opening a browser for cluster logins").Envar(bindAddrEnvVar).StringVar(&cf.BindAddr) app.Flag("callback", "Override the base URL (host:port) of the link shown when opening a browser for cluster logins. Must be used with --bind-addr.").StringVar(&cf.CallbackAddr) app.Flag("browser-login", browserHelp).Hidden().Envar(browserEnvVar).StringVar(&cf.Browser) - modes := []string{mfaModeAuto, mfaModeCrossPlatform, mfaModePlatform, mfaModeOTP} + modes := []string{mfaModeAuto, mfaModeCrossPlatform, mfaModePlatform, mfaModeOTP, mfaModeSSO} app.Flag("mfa-mode", fmt.Sprintf("Preferred mode for MFA and Passwordless assertions (%v)", strings.Join(modes, ", "))). Default(mfaModeAuto). Envar(mfaModeEnvVar). @@ -4210,6 +4212,7 @@ func loadClientConfigFromCLIConf(cf *CLIConf, proxy string) (*client.Config, err } c.AuthenticatorAttachment = mfaOpts.AuthenticatorAttachment c.PreferOTP = mfaOpts.PreferOTP + c.PreferSSO = mfaOpts.PreferSSO // If agent forwarding was specified on the command line enable it. c.ForwardAgent = options.ForwardAgent @@ -4391,6 +4394,7 @@ func (c *CLIConf) GetProfile() (*profile.Profile, error) { type mfaModeOpts struct { AuthenticatorAttachment wancli.AuthenticatorAttachment PreferOTP bool + PreferSSO bool } func parseMFAMode(mode string) (*mfaModeOpts, error) { @@ -4403,6 +4407,8 @@ func parseMFAMode(mode string) (*mfaModeOpts, error) { opts.AuthenticatorAttachment = wancli.AttachmentPlatform case mfaModeOTP: opts.PreferOTP = true + case mfaModeSSO: + opts.PreferSSO = true default: return nil, fmt.Errorf("invalid MFA mode: %q", mode) }