Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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,
},
Expand Down
57 changes: 48 additions & 9 deletions lib/client/mfa/cli.go
Comment thread
Joerger marked this conversation as resolved.
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 @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do appreciate the small change but it looks like we are mainly adding dead code to this file. Would it make sense to do a bit more here, so at least we can test the "SSO not implemented" error? Or maybe move some changes to a PR where they are effective?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, in that case I'll merge this into #46982


// 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)
}
Comment thread
Joerger marked this conversation as resolved.
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=<sso,otp>.\n\n"
fmt.Fprintf(c.Writer, msg, strings.Join(availableMethods, ", "), strings.Join(chosenMethods, "and "))
}

switch {
Expand All @@ -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, ", "))
}
}

Expand Down
8 changes: 7 additions & 1 deletion tool/tsh/common/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ const (
mfaModePlatform = "platform"
// mfaModeOTP utilizes only OTP devices.
mfaModeOTP = "otp"
// mfaModeSSO utilizes only SSO devices.
mfaModeSSO = "sso"
)

const (
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down