Skip to content
Merged
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 api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const (
// Github means authentication will happen remotely using a Github connector.
Github = "github"

// BrowserMFA is for CLI flows that delegate MFA to the browser.
BrowserMFA = "browser_mfa"

// HumanDateFormatSeconds is a human readable date formatting with seconds
HumanDateFormatSeconds = "Jan 2 2006 15:04:05 UTC"

Expand Down
2 changes: 1 addition & 1 deletion api/proto/teleport/legacy/types/mfa_device.proto
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,6 @@ message SSOMFADevice {
}

// BrowserMFADevice is a synthetic MFA device that is made available if a user
// has at least one WebAuthn device and no SSO setup. This message doesn't
// has at least one WebAuthn device and no SSO MFA setup. This message doesn't
// require any fields, it just needs to exist so it can be an MFA option.
message BrowserMFADevice {}
2 changes: 1 addition & 1 deletion api/types/mfa_device.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gen/proto/ts/teleport/legacy/types/mfa_device_pb.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 44 additions & 4 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4346,7 +4346,16 @@ func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.Cre
}
}

challenges, err := a.mfaAuthChallenge(ctx, username, req.SSOClientRedirectURL, req.ProxyAddress, challengeExtensions)
// When completing a Browser MFA flow, only a WebAuthn challenge is needed, so
// clear the redirect URL so SSO and Browser MFA challenges are not generated.
clientRedirectURL := ""
if req.BrowserMFARequestID == "" {
// Both SSO and Browser MFA redirect URLs point to the same callback server on tsh.
// So we can take either one and generate an auth challenge with it.
clientRedirectURL = cmp.Or(req.SSOClientRedirectURL, req.BrowserMFATSHRedirectURL)
}
Comment thread
Joerger marked this conversation as resolved.

challenges, err := a.mfaAuthChallenge(ctx, username, clientRedirectURL, req.ProxyAddress, challengeExtensions)
if err != nil {
// Do not obfuscate config-related errors.
if errors.Is(err, types.ErrPasswordlessRequiresWebauthn) || errors.Is(err, types.ErrPasswordlessDisabledBySettings) {
Expand Down Expand Up @@ -7882,7 +7891,7 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck

// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
// registered by the user.
func (a *Server) mfaAuthChallenge(ctx context.Context, user, ssoClientRedirectURL, proxyAddress string, challengeExtensions *mfav1.ChallengeExtensions) (*proto.MFAAuthenticateChallenge, error) {
func (a *Server) mfaAuthChallenge(ctx context.Context, user, clientRedirectURL, proxyAddress string, challengeExtensions *mfav1.ChallengeExtensions) (*proto.MFAAuthenticateChallenge, error) {
isPasswordless := challengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN

// Check what kind of MFA is enabled.
Expand All @@ -7893,6 +7902,7 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user, ssoClientRedirectUR
enableTOTP := apref.IsSecondFactorTOTPAllowed()
enableWebauthn := apref.IsSecondFactorWebauthnAllowed()
enableSSO := apref.IsSecondFactorSSOAllowed()
enableBrowserMFA := apref.GetAllowCLIAuthViaBrowser()

// Fetch configurations. The IsSecondFactor*Allowed calls above already
// include the necessary checks of config empty, disabled, etc.
Expand Down Expand Up @@ -7988,13 +7998,13 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user, ssoClientRedirectUR

// If the user has an SSO device and the client provided a redirect URL to handle
// the MFA SSO flow, create an SSO challenge.
if enableSSO && groupedDevs.SSO != nil && ssoClientRedirectURL != "" {
if enableSSO && groupedDevs.SSO != nil && clientRedirectURL != "" {
if challenge.SSOChallenge, err = a.BeginSSOMFAChallenge(
ctx,
mfatypes.BeginSSOMFAChallengeParams{
User: user,
SSO: groupedDevs.SSO.GetSso(),
SSOClientRedirectURL: ssoClientRedirectURL,
SSOClientRedirectURL: clientRedirectURL,
ProxyAddress: proxyAddress,
Ext: challengeExtensions,
},
Expand All @@ -8003,6 +8013,23 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user, ssoClientRedirectUR
}
}

// If the user has a WebAuthn device, return a Browser MFA challenge. This
// challenge is useful in cases where a user only has a browser-associated
// WebAuthn device, but is trying to MFA via a CLI tool (tsh, tctl etc.)
if enableBrowserMFA && groupedDevs.Browser != nil && clientRedirectURL != "" {
if challenge.BrowserMFAChallenge, err = a.BeginBrowserMFAChallenge(
ctx,
mfatypes.BeginBrowserMFAChallengeParams{
User: user,
BrowserMFATSHRedirectURL: clientRedirectURL,
ProxyAddress: proxyAddress,
Ext: challengeExtensions,
},
); err != nil {
return nil, trace.Wrap(err)
}
}

clusterName, err := a.GetClusterName(ctx)
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -8029,6 +8056,7 @@ type devicesByType struct {
TOTP bool
Webauthn []*types.MFADevice
SSO *types.MFADevice
Browser *types.MFADevice
}

func groupByDeviceType(devs []*types.MFADevice) devicesByType {
Expand All @@ -8047,6 +8075,18 @@ func groupByDeviceType(devs []*types.MFADevice) devicesByType {
logger.WarnContext(context.Background(), "Skipping MFA device with unknown type", "device_type", logutils.TypeAttr(dev.Device))
}
}

// Create a synthetic Browser device if the user has a WebAuthn device.
// This enables browser-based MFA for users who have WebAuthn/passkey devices.
if len(res.Webauthn) > 0 {
res.Browser = &types.MFADevice{
Id: "browser",
Device: &types.MFADevice_Browser{
Browser: &types.BrowserMFADevice{},
},
}
}

return res
}

Expand Down
44 changes: 43 additions & 1 deletion lib/auth/browser_mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,18 @@ import (
"context"
"net/url"

"github.com/google/uuid"
"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/auth/internal/browsermfa"
"github.com/gravitational/teleport/lib/auth/mfatypes"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/client/sso"
"github.com/gravitational/teleport/lib/services"
)

// CompleteBrowserMFAChallenge completes an MFA challenge response by returning the redirect URL with encrypted response.
Expand All @@ -49,7 +55,7 @@ func (a *Server) CompleteBrowserMFAChallenge(ctx context.Context, requestID stri
}

// Valid WebAuthn response, encrypt and return it
u, err := url.Parse(mfaSession.ClientRedirectURL)
u, err := url.Parse(mfaSession.TSHRedirectURL)
if err != nil {
return "", trace.Wrap(err)
}
Expand All @@ -62,3 +68,39 @@ func (a *Server) CompleteBrowserMFAChallenge(ctx context.Context, requestID stri

return clientRedirectURL, nil
}

// BeginBrowserMFAChallenge creates a new Browser MFA auth request and session
// data for the given params and stores it in the backend.
func (a *Server) BeginBrowserMFAChallenge(ctx context.Context, params mfatypes.BeginBrowserMFAChallengeParams) (*proto.BrowserMFAChallenge, error) {
if err := sso.ValidateClientRedirect(params.BrowserMFATSHRedirectURL, sso.CeremonyTypeMFA, nil); err != nil {
return nil, trace.Wrap(err, InvalidClientRedirectErrorMessage)
}

requestID := uuid.NewString()
browserChal := &proto.BrowserMFAChallenge{
RequestId: requestID,
}

sessionData := &services.MFASessionData{
Username: params.User,
RequestID: requestID,
ConnectorID: constants.BrowserMFA,
ConnectorType: constants.BrowserMFA,
TSHRedirectURL: params.BrowserMFATSHRedirectURL,
ChallengeExtensions: &mfatypes.ChallengeExtensions{
Scope: params.Ext.Scope,
AllowReuse: params.Ext.AllowReuse,
},
SourceCluster: params.SourceCluster,
TargetCluster: params.TargetCluster,
Payload: &mfatypes.SessionIdentifyingPayload{
SSHSessionID: params.SIP.GetSshSessionId(),
},
}

if err := a.UpsertMFASessionData(ctx, sessionData); err != nil {
return nil, trace.Wrap(err)
}

return browserChal, nil
}
Loading
Loading