Skip to content
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

Deprecate /webapi/ssh/certs + Use 2-step MFA login flow for Teleport Connect #47153

Merged
merged 7 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
62 changes: 21 additions & 41 deletions gen/proto/go/teleport/lib/teleterm/v1/auth_settings.pb.go

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

28 changes: 0 additions & 28 deletions gen/proto/ts/teleport/lib/teleterm/v1/auth_settings_pb.ts

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

73 changes: 3 additions & 70 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -3919,77 +3919,11 @@ func (tc *TeleportClient) pwdlessLogin(ctx context.Context, keyRing *KeyRing) (*
return response, trace.Wrap(err)
}

func (tc *TeleportClient) localLogin(ctx context.Context, keyRing *KeyRing, secondFactor constants.SecondFactorType) (*authclient.SSHLoginResponse, error) {
var err error
var response *authclient.SSHLoginResponse

// TODO(awly): mfa: ideally, clients should always go through mfaLocalLogin
// (with a nop MFA challenge if no 2nd factor is required). That way we can
// deprecate the direct login endpoint.
switch secondFactor {
case constants.SecondFactorOff, constants.SecondFactorOTP:
response, err = tc.directLogin(ctx, secondFactor, keyRing)
if err != nil {
return nil, trace.Wrap(err)
}
case constants.SecondFactorU2F, constants.SecondFactorWebauthn, constants.SecondFactorOn, constants.SecondFactorOptional:
response, err = tc.mfaLocalLogin(ctx, keyRing)
if err != nil {
return nil, trace.Wrap(err)
}
default:
return nil, trace.BadParameter("unsupported second factor type: %q", secondFactor)
}

// Ignore username returned from proxy
response.Username = ""
return response, nil
}

// directLogin asks for a password + OTP token, makes a request to CA via proxy
func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType constants.SecondFactorType, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/directLogin",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()

password, err := tc.AskPassword(ctx)
if err != nil {
return nil, trace.Wrap(err)
}

// Only ask for a second factor if it's enabled.
var otpToken string
if secondFactorType == constants.SecondFactorOTP {
otpToken, err = tc.AskOTP(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}

sshLogin, err := tc.NewSSHLogin(keyRing)
if err != nil {
return nil, trace.Wrap(err)
}

// Ask the CA (via proxy) to sign our public key:
response, err := SSHAgentLogin(ctx, SSHLoginDirect{
SSHLogin: sshLogin,
User: tc.Username,
Password: password,
OTPToken: otpToken,
})

return response, trace.Wrap(err)
}

// mfaLocalLogin asks for a password and performs the challenge-response authentication
func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, keyRing *KeyRing) (*authclient.SSHLoginResponse, error) {
// localLogin asks for a password and performs an MFA ceremony.
func (tc *TeleportClient) localLogin(ctx context.Context, keyRing *KeyRing, _ constants.SecondFactorType) (*authclient.SSHLoginResponse, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/mfaLocalLogin",
"teleportClient/localLogin",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()
Expand All @@ -4010,7 +3944,6 @@ func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, keyRing *KeyRing) (
Password: password,
MFAPromptConstructor: tc.NewMFAPrompt,
})

return response, trace.Wrap(err)
}

Expand Down
77 changes: 40 additions & 37 deletions lib/client/weblogin.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,36 @@ func (r *CreateSSHCertReq) CheckAndSetDefaults() error {
return trace.Wrap(r.UserPublicKeys.CheckAndSetDefaults())
}

// HeadlessLoginReq is a headless login request for /webapi/headless/login.
type HeadlessLoginReq struct {
// User is a teleport username
User string `json:"user"`
// HeadlessAuthenticationID is a headless authentication resource id.
HeadlessAuthenticationID string `json:"headless_id"`
// UserPublicKeys is embedded and holds user SSH and TLS public keys that
// should be used as the subject of issued certificates, and optional
// hardware key attestation statements for each key.
UserPublicKeys
TTL time.Duration `json:"ttl"`
// Compatibility specifies OpenSSH compatibility flags.
Compatibility string `json:"compatibility,omitempty"`
// RouteToCluster is an optional cluster name to route the response
// credentials to.
RouteToCluster string
// KubernetesCluster is an optional k8s cluster name to route the response
// credentials to.
KubernetesCluster string
}

// CheckAndSetDefaults checks and sets default values.
func (r *HeadlessLoginReq) CheckAndSetDefaults() error {
if r.HeadlessAuthenticationID == "" {
return trace.BadParameter("missing headless authentication id for headless login")
}

return trace.Wrap(r.UserPublicKeys.CheckAndSetDefaults())
}

// UserPublicKeys holds user-submitted public keys and attestation statements
// used in local login requests.
type UserPublicKeys struct {
Expand Down Expand Up @@ -565,41 +595,6 @@ func SSHAgentSSOLogin(ctx context.Context, login SSHLoginSSO, config *Redirector
}
}

// SSHAgentLogin is used by tsh to fetch local user credentials.
func SSHAgentLogin(ctx context.Context, login SSHLoginDirect) (*authclient.SSHLoginResponse, error) {
clt, _, err := initClient(login.ProxyAddr, login.Insecure, login.Pool, login.ExtraHeaders)
if err != nil {
return nil, trace.Wrap(err)
}

re, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "ssh", "certs"), CreateSSHCertReq{
User: login.User,
Password: login.Password,
OTPToken: login.OTPToken,
UserPublicKeys: UserPublicKeys{
SSHPubKey: login.SSHPubKey,
TLSPubKey: login.TLSPubKey,
SSHAttestationStatement: login.SSHAttestationStatement,
TLSAttestationStatement: login.TLSAttestationStatement,
},
TTL: login.TTL,
Compatibility: login.Compatibility,
RouteToCluster: login.RouteToCluster,
KubernetesCluster: login.KubernetesCluster,
})
if err != nil {
return nil, trace.Wrap(err)
}

var out authclient.SSHLoginResponse
err = json.Unmarshal(re.Bytes(), &out)
if err != nil {
return nil, trace.Wrap(err)
}

return &out, nil
}

// OpenURLInBrowser opens a URL in a web browser.
func OpenURLInBrowser(browser string, URL string) error {
var execCmd *exec.Cmd
Expand Down Expand Up @@ -644,7 +639,7 @@ func SSHAgentHeadlessLogin(ctx context.Context, login SSHLoginHeadless) (*authcl
// This request will block until the headless login is approved.
clt.Client.HTTPClient().Timeout = defaults.HeadlessLoginTimeout

re, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "ssh", "certs"), CreateSSHCertReq{
req := HeadlessLoginReq{
User: login.User,
HeadlessAuthenticationID: login.HeadlessAuthenticationID,
UserPublicKeys: UserPublicKeys{
Expand All @@ -657,7 +652,14 @@ func SSHAgentHeadlessLogin(ctx context.Context, login SSHLoginHeadless) (*authcl
Compatibility: login.Compatibility,
RouteToCluster: login.RouteToCluster,
KubernetesCluster: login.KubernetesCluster,
})
}

re, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "headless", "login"), req)
if trace.IsNotFound(err) {
// fallback to deprecated headless login endpoint
// TODO(Joerger): DELETE IN v18.0.0
re, err = clt.PostJSON(ctx, clt.Endpoint("webapi", "ssh", "certs"), req)
}
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -781,6 +783,7 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*authclient.SSHLo
RouteToCluster: login.RouteToCluster,
KubernetesCluster: login.KubernetesCluster,
}

// Convert back from auth gRPC proto response.
switch r := mfaResp.Response.(type) {
case *proto.MFAAuthenticateResponse_TOTP:
Expand Down
9 changes: 1 addition & 8 deletions lib/teleterm/apiserver/handler/handler_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (

// Login logs in a user to a cluster
func (s *Handler) Login(ctx context.Context, req *api.LoginRequest) (*api.EmptyResponse, error) {
cluster, clusterClient, err := s.DaemonService.ResolveCluster(req.ClusterUri)
cluster, _, err := s.DaemonService.ResolveCluster(req.ClusterUri)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -37,11 +37,6 @@ func (s *Handler) Login(ctx context.Context, req *api.LoginRequest) (*api.EmptyR
return nil, trace.BadParameter("cluster URI must be a root URI")
}

// The credentials + MFA login flow in the Electron app assumes that the default CLI prompt is
// used and works around that. Thus we have to remove the teleterm-specific MFAPromptConstructor
// added by daemon.Service.ResolveClusterURI.
clusterClient.MFAPromptConstructor = nil

if err = s.DaemonService.ClearCachedClientsForRoot(cluster.URI); err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -137,8 +132,6 @@ func (s *Handler) GetAuthSettings(ctx context.Context, req *api.GetAuthSettingsR
}

result := &api.AuthSettings{
PreferredMfa: string(preferences.PreferredLocalMFA),
SecondFactor: string(preferences.SecondFactor),
LocalAuthEnabled: preferences.LocalAuthEnabled,
AuthType: preferences.AuthType,
AllowPasswordless: preferences.AllowPasswordless,
Expand Down
Loading
Loading