diff --git a/gen/proto/go/teleport/lib/teleterm/v1/auth_settings.pb.go b/gen/proto/go/teleport/lib/teleterm/v1/auth_settings.pb.go index cec1afc4ca24..ffa26e30e76f 100644 --- a/gen/proto/go/teleport/lib/teleterm/v1/auth_settings.pb.go +++ b/gen/proto/go/teleport/lib/teleterm/v1/auth_settings.pb.go @@ -45,10 +45,6 @@ type AuthSettings struct { // local_auth_enabled is a flag that enables local authentication LocalAuthEnabled bool `protobuf:"varint,1,opt,name=local_auth_enabled,json=localAuthEnabled,proto3" json:"local_auth_enabled,omitempty"` - // second_factor is the type of second factor to use in authentication. - SecondFactor string `protobuf:"bytes,2,opt,name=second_factor,json=secondFactor,proto3" json:"second_factor,omitempty"` - // preferred_mfa is the prefered mfa for local logins - PreferredMfa string `protobuf:"bytes,3,opt,name=preferred_mfa,json=preferredMfa,proto3" json:"preferred_mfa,omitempty"` // auth_providers contains a list of auth providers AuthProviders []*AuthProvider `protobuf:"bytes,4,rep,name=auth_providers,json=authProviders,proto3" json:"auth_providers,omitempty"` // has_message_of_the_day is a flag indicating that the cluster has MOTD @@ -102,20 +98,6 @@ func (x *AuthSettings) GetLocalAuthEnabled() bool { return false } -func (x *AuthSettings) GetSecondFactor() string { - if x != nil { - return x.SecondFactor - } - return "" -} - -func (x *AuthSettings) GetPreferredMfa() string { - if x != nil { - return x.PreferredMfa - } - return "" -} - func (x *AuthSettings) GetAuthProviders() []*AuthProvider { if x != nil { return x.AuthProviders @@ -226,32 +208,30 @@ var file_teleport_lib_teleterm_v1_auth_settings_proto_rawDesc = []byte{ 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x73, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x18, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x22, 0x87, 0x03, 0x0a, 0x0c, 0x41, 0x75, 0x74, + 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x22, 0xe7, 0x02, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x41, 0x75, 0x74, 0x68, - 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x65, 0x63, 0x6f, 0x6e, - 0x64, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x23, 0x0a, 0x0d, - 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x6d, 0x66, 0x61, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x4d, 0x66, - 0x61, 0x12, 0x4d, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, - 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, - 0x12, 0x32, 0x0a, 0x16, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, - 0x6f, 0x66, 0x5f, 0x74, 0x68, 0x65, 0x5f, 0x64, 0x61, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x12, 0x68, 0x61, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x66, 0x54, 0x68, - 0x65, 0x44, 0x61, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x61, - 0x6c, 0x6c, 0x6f, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, - 0x12, 0x30, 0x0a, 0x14, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, - 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x4e, 0x61, - 0x6d, 0x65, 0x22, 0x59, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x4d, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x26, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6c, 0x69, 0x62, 0x2e, 0x74, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x72, 0x6d, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x32, 0x0a, 0x16, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x6f, 0x66, 0x5f, 0x74, 0x68, 0x65, 0x5f, 0x64, 0x61, 0x79, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x68, 0x61, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x4f, 0x66, 0x54, 0x68, 0x65, 0x44, 0x61, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x75, + 0x74, 0x68, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, + 0x75, 0x74, 0x68, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2d, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x6c, 0x65, 0x73, 0x73, 0x12, 0x30, 0x0a, 0x14, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, + 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x43, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x4a, 0x04, + 0x08, 0x03, 0x10, 0x04, 0x52, 0x0d, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x5f, 0x66, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x6d, + 0x66, 0x61, 0x22, 0x59, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, diff --git a/gen/proto/ts/teleport/lib/teleterm/v1/auth_settings_pb.ts b/gen/proto/ts/teleport/lib/teleterm/v1/auth_settings_pb.ts index 6693b28b966b..03ea813fdfe9 100644 --- a/gen/proto/ts/teleport/lib/teleterm/v1/auth_settings_pb.ts +++ b/gen/proto/ts/teleport/lib/teleterm/v1/auth_settings_pb.ts @@ -42,18 +42,6 @@ export interface AuthSettings { * @generated from protobuf field: bool local_auth_enabled = 1; */ localAuthEnabled: boolean; - /** - * second_factor is the type of second factor to use in authentication. - * - * @generated from protobuf field: string second_factor = 2; - */ - secondFactor: string; - /** - * preferred_mfa is the prefered mfa for local logins - * - * @generated from protobuf field: string preferred_mfa = 3; - */ - preferredMfa: string; /** * auth_providers contains a list of auth providers * @@ -118,8 +106,6 @@ class AuthSettings$Type extends MessageType { constructor() { super("teleport.lib.teleterm.v1.AuthSettings", [ { no: 1, name: "local_auth_enabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, - { no: 2, name: "second_factor", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, - { no: 3, name: "preferred_mfa", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, { no: 4, name: "auth_providers", kind: "message", repeat: 1 /*RepeatType.PACKED*/, T: () => AuthProvider }, { no: 5, name: "has_message_of_the_day", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }, { no: 6, name: "auth_type", kind: "scalar", T: 9 /*ScalarType.STRING*/ }, @@ -130,8 +116,6 @@ class AuthSettings$Type extends MessageType { create(value?: PartialMessage): AuthSettings { const message = globalThis.Object.create((this.messagePrototype!)); message.localAuthEnabled = false; - message.secondFactor = ""; - message.preferredMfa = ""; message.authProviders = []; message.hasMessageOfTheDay = false; message.authType = ""; @@ -149,12 +133,6 @@ class AuthSettings$Type extends MessageType { case /* bool local_auth_enabled */ 1: message.localAuthEnabled = reader.bool(); break; - case /* string second_factor */ 2: - message.secondFactor = reader.string(); - break; - case /* string preferred_mfa */ 3: - message.preferredMfa = reader.string(); - break; case /* repeated teleport.lib.teleterm.v1.AuthProvider auth_providers */ 4: message.authProviders.push(AuthProvider.internalBinaryRead(reader, reader.uint32(), options)); break; @@ -185,12 +163,6 @@ class AuthSettings$Type extends MessageType { /* bool local_auth_enabled = 1; */ if (message.localAuthEnabled !== false) writer.tag(1, WireType.Varint).bool(message.localAuthEnabled); - /* string second_factor = 2; */ - if (message.secondFactor !== "") - writer.tag(2, WireType.LengthDelimited).string(message.secondFactor); - /* string preferred_mfa = 3; */ - if (message.preferredMfa !== "") - writer.tag(3, WireType.LengthDelimited).string(message.preferredMfa); /* repeated teleport.lib.teleterm.v1.AuthProvider auth_providers = 4; */ for (let i = 0; i < message.authProviders.length; i++) AuthProvider.internalBinaryWrite(message.authProviders[i], writer.tag(4, WireType.LengthDelimited).fork(), options).join(); diff --git a/lib/client/api.go b/lib/client/api.go index 8a4625a0a32d..016b7537b60b 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -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() @@ -4010,7 +3944,6 @@ func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, keyRing *KeyRing) ( Password: password, MFAPromptConstructor: tc.NewMFAPrompt, }) - return response, trace.Wrap(err) } diff --git a/lib/client/mfa/prompt.go b/lib/client/mfa/prompt.go index 93a9645e2faf..ab99c184ebfc 100644 --- a/lib/client/mfa/prompt.go +++ b/lib/client/mfa/prompt.go @@ -93,7 +93,7 @@ func (c PromptConfig) GetRunOptions(ctx context.Context, chal *proto.MFAAuthenti // Does the current platform support hardware MFA? Adjust accordingly. switch { - case !promptTOTP && !c.WebauthnSupported: + case !promptTOTP && promptWebauthn && !c.WebauthnSupported: return RunOpts{}, trace.BadParameter("hardware device MFA not supported by your platform, please register an OTP device") case !c.WebauthnSupported: // Do not prompt for hardware devices, it won't work. diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 5ad373a88409..a7d24341f65d 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -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 { @@ -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 @@ -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{ @@ -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) } @@ -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: diff --git a/lib/teleterm/apiserver/handler/handler_auth.go b/lib/teleterm/apiserver/handler/handler_auth.go index 30a4a5d83fc3..8be49eea5260 100644 --- a/lib/teleterm/apiserver/handler/handler_auth.go +++ b/lib/teleterm/apiserver/handler/handler_auth.go @@ -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) } @@ -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) } @@ -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, diff --git a/lib/teleterm/clusters/cluster_auth.go b/lib/teleterm/clusters/cluster_auth.go index ee0d7d1b58e8..2793e27b722c 100644 --- a/lib/teleterm/clusters/cluster_auth.go +++ b/lib/teleterm/clusters/cluster_auth.go @@ -89,39 +89,9 @@ func (c *Cluster) Logout(ctx context.Context) error { // LocalLogin processes local logins for this cluster func (c *Cluster) LocalLogin(ctx context.Context, user, password, otpToken string) error { - pingResp, err := c.updateClientFromPingResponse(ctx) - if err != nil { - return trace.Wrap(err) - } - c.clusterClient.AuthConnector = constants.LocalConnector - var sshLoginFunc client.SSHLoginFunc - switch pingResp.Auth.SecondFactor { - case constants.SecondFactorOff, constants.SecondFactorOTP: - sshLoginFunc = c.localLogin(user, password, otpToken) - case constants.SecondFactorU2F, constants.SecondFactorWebauthn: - sshLoginFunc = c.localMFALogin(user, password) - case constants.SecondFactorOn, constants.SecondFactorOptional: - // tsh always uses client.SSHAgentMFALogin for any `second_factor` option other than `off` and - // `otp`. If it's set to `on` or `optional` and it turns out the user wants to use an OTP, it - // bails out to stdin to ask them for it. - // - // Connect cannot do that, but it still wants to use the auth code from lib/client that it - // shares with tsh. So to temporarily work around this problem, we check if the OTP token was - // submitted. If yes, then we use client.SSHAgentLogin, which lets us provide the token and skip - // asking for it over stdin. If not, we use client.SSHAgentMFALogin which should handle auth - // methods that don't use OTP. - if otpToken != "" { - sshLoginFunc = c.localLogin(user, password, otpToken) - } else { - sshLoginFunc = c.localMFALogin(user, password) - } - default: - return trace.BadParameter("unsupported second factor type: %q", pingResp.Auth.SecondFactor) - } - - if err := c.login(ctx, sshLoginFunc); err != nil { + if err := c.login(ctx, c.localMFALogin(user, password)); err != nil { return trace.Wrap(err) } @@ -241,26 +211,6 @@ func (c *Cluster) localMFALogin(user, password string) client.SSHLoginFunc { } } -func (c *Cluster) localLogin(user, password, otpToken string) client.SSHLoginFunc { - return func(ctx context.Context, keyRing *client.KeyRing) (*authclient.SSHLoginResponse, error) { - sshLogin, err := c.clusterClient.NewSSHLogin(keyRing) - if err != nil { - return nil, trace.Wrap(err) - } - - response, err := client.SSHAgentLogin(ctx, client.SSHLoginDirect{ - SSHLogin: sshLogin, - User: user, - Password: password, - OTPToken: otpToken, - }) - if err != nil { - return nil, trace.Wrap(err) - } - return response, nil - } -} - func (c *Cluster) ssoLogin(providerType, providerName string) client.SSHLoginFunc { return c.clusterClient.SSOLoginFn(providerName, providerName, providerType) } diff --git a/lib/teleterm/daemon/mfaprompt.go b/lib/teleterm/daemon/mfaprompt.go index f33edb17f5fa..81f53b9ab800 100644 --- a/lib/teleterm/daemon/mfaprompt.go +++ b/lib/teleterm/daemon/mfaprompt.go @@ -83,16 +83,16 @@ func (p *mfaPrompt) Run(ctx context.Context, chal *proto.MFAAuthenticateChalleng return &proto.MFAAuthenticateResponse{}, nil } - // Depending on the run opts, we may spawn a TOTP goroutine, webauth goroutine, or both. + // Depending on the run opts, we may spawn an TOTP goroutine, webauth goroutine, or both. spawnGoroutines := func(ctx context.Context, wg *sync.WaitGroup, respC chan<- libmfa.MFAGoroutineResponse) { ctx, cancel := context.WithCancelCause(ctx) - // Fire App goroutine (TOTP). + // Fire app Prompt goroutine. Handles client cancellation and TOTP. wg.Add(1) go func() { defer wg.Done() - resp, err := p.promptMFA(ctx, chal, runOpts) + resp, err := p.promptMFA(ctx, runOpts) respC <- libmfa.MFAGoroutineResponse{Resp: resp, Err: err} // If the user closes the modal in the Electron app, we need to be able to cancel the other @@ -128,7 +128,7 @@ func (p *mfaPrompt) promptWebauthn(ctx context.Context, chal *proto.MFAAuthentic return resp, nil } -func (p *mfaPrompt) promptMFA(ctx context.Context, chal *proto.MFAAuthenticateChallenge, runOpts libmfa.RunOpts) (*proto.MFAAuthenticateResponse, error) { +func (p *mfaPrompt) promptMFA(ctx context.Context, runOpts libmfa.RunOpts) (*proto.MFAAuthenticateResponse, error) { resp, err := p.promptAppMFA(ctx, &api.PromptMFARequest{ ClusterUri: p.resourceURI.GetClusterURI().String(), Reason: p.cfg.PromptReason, diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 873d2d9bfb1a..fff947c6cb4f 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -406,7 +406,6 @@ func (h *APIHandler) handlePreflight(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusOK) - } // Check if this request should be forwarded to an application handler to @@ -778,8 +777,11 @@ func (h *Handler) bindDefaultEndpoints() { h.POST("/webapi/users/privilege/token", h.WithAuth(h.createPrivilegeTokenHandle)) // Issues SSH temp certificates based on 2FA access creds + // TODO(Joerger): DELETE IN v18.0.0, deprecated in favor of mfa login endpoints. h.POST("/webapi/ssh/certs", h.WithUnauthenticatedLimiter(h.createSSHCert)) + h.POST("/webapi/headless/login", h.WithUnauthenticatedLimiter(h.headlessLogin)) + // list available sites h.GET("/webapi/sites", h.WithAuth(h.getClusters)) @@ -4077,6 +4079,8 @@ func (h *Handler) hostCredentials(w http.ResponseWriter, r *http.Request, p http // # Success response // // { "cert": "base64 encoded signed cert", "host_signers": [{"domain_name": "example.com", "checking_keys": ["base64 encoded public signing key"]}] } +// +// TODO(Joerger): DELETE IN v18.0.0, deprecated in favor of mfa login endpoints. func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { var req client.CreateSSHCertReq if err := httplib.ReadJSON(r, &req); err != nil { @@ -4157,6 +4161,69 @@ func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httpro return loginResp, nil } +// headlessLogin is a web call that perform headless login based on a user's +// name, returning new login certs if successful. +// +// POST /v1/webapi/headless/login +// +// { "user": "bob", "pub_key": "key to sign", "ttl": 1000000000 } +// +// # Success response +// +// { "cert": "base64 encoded signed cert", "host_signers": [{"domain_name": "example.com", "checking_keys": ["base64 encoded public signing key"]}] } +func (h *Handler) headlessLogin(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + var req client.HeadlessLoginReq + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + authClient := h.cfg.ProxyClient + + authSSHUserReq := authclient.AuthenticateSSHRequest{ + AuthenticateUserRequest: authclient.AuthenticateUserRequest{ + Username: req.User, + SSHPublicKey: req.SSHPubKey, + TLSPublicKey: req.TLSPubKey, + ClientMetadata: clientMetaFromReq(r), + HeadlessAuthenticationID: req.HeadlessAuthenticationID, + }, + CompatibilityMode: req.Compatibility, + TTL: req.TTL, + RouteToCluster: req.RouteToCluster, + KubernetesCluster: req.KubernetesCluster, + SSHAttestationStatement: req.SSHAttestationStatement, + TLSAttestationStatement: req.TLSAttestationStatement, + } + + // We need to use the default callback timeout rather than the standard client timeout. + // However, authClient is shared across all Proxy->Auth requests, so we need to create + // a new client to avoid applying the callback timeout to other concurrent requests. To + // this end, we create a clone of the HTTP Client with the desired timeout instead. + httpClient, err := authClient.CloneHTTPClient( + authclient.ClientParamTimeout(defaults.HeadlessLoginTimeout), + authclient.ClientParamResponseHeaderTimeout(defaults.HeadlessLoginTimeout), + ) + if err != nil { + return nil, trace.Wrap(err) + } + + // HTTP server has shorter WriteTimeout than is needed, so we override WriteDeadline of the connection. + if conn, err := authz.ConnFromContext(r.Context()); err == nil { + if err := conn.SetWriteDeadline(h.clock.Now().Add(defaults.HeadlessLoginTimeout)); err != nil { + return nil, trace.Wrap(err) + } + } + + loginResp, err := httpClient.AuthenticateSSHUser(r.Context(), authSSHUserReq) + if err != nil { + return nil, trace.Wrap(err) + } + return loginResp, nil +} + // validateTrustedCluster validates the token for a trusted cluster and returns it's own host and user certificate authority. // // POST /webapi/trustedclusters/validate diff --git a/lib/web/apiserver_login_test.go b/lib/web/apiserver_login_test.go index c26a177ba20a..40299db5d3d3 100644 --- a/lib/web/apiserver_login_test.go +++ b/lib/web/apiserver_login_test.go @@ -667,6 +667,7 @@ func configureClusterForMFA(t *testing.T, env *webPack, spec *types.AuthPreferen // TestCreateSSHCert tests the login endpoint /webapi/ssh/certs with different // options for subject SSH and TLS keys. +// TODO(Joerger): DELETE IN v18.0.0 when 2fa-less login endpoint is deprecated. func TestCreateSSHCert(t *testing.T) { t.Parallel() @@ -754,7 +755,6 @@ func TestCreateSSHCert(t *testing.T) { validateSSHLoginResponse(t, resp.Bytes(), tc.expectSubjectSSHPub, tc.expectSubjectTLSPub) }) } - } func validateSSHLoginResponse(t *testing.T, resp []byte, expectedSubjectSSHPub ssh.PublicKey, expectedSubjectTLSPub crypto.PublicKey) *authclient.SSHLoginResponse { diff --git a/proto/teleport/lib/teleterm/v1/auth_settings.proto b/proto/teleport/lib/teleterm/v1/auth_settings.proto index 18bed556ae0e..4b790fb61116 100644 --- a/proto/teleport/lib/teleterm/v1/auth_settings.proto +++ b/proto/teleport/lib/teleterm/v1/auth_settings.proto @@ -26,10 +26,13 @@ option go_package = "github.com/gravitational/teleport/gen/proto/go/teleport/lib message AuthSettings { // local_auth_enabled is a flag that enables local authentication bool local_auth_enabled = 1; - // second_factor is the type of second factor to use in authentication. - string second_factor = 2; - // preferred_mfa is the prefered mfa for local logins - string preferred_mfa = 3; + + reserved "second_factor"; + reserved 2; // second_factor + + reserved "preferred_mfa"; + reserved 3; // preferred_mfa + // auth_providers contains a list of auth providers repeated AuthProvider auth_providers = 4; // has_message_of_the_day is a flag indicating that the cluster has MOTD diff --git a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts index facfe4853b27..6e7e43f50b0b 100644 --- a/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/web/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -83,8 +83,6 @@ export class MockTshClient implements TshdClient { getAuthSettings = () => new MockedUnaryCall({ localAuthEnabled: true, - secondFactor: 'webauthn', - preferredMfa: 'webauthn', authProviders: [], hasMessageOfTheDay: false, authType: 'local', diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx index bd1541cb499e..f7c2e53087b1 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx @@ -50,11 +50,9 @@ function makeProps(): ClusterLoginPresentationProps { status: 'success', statusText: '', data: { - preferredMfa: 'webauthn', localAuthEnabled: true, authProviders: [], type: '', - secondFactor: 'optional', hasMessageOfTheDay: false, allowPasswordless: true, localConnectorName: '', @@ -109,7 +107,6 @@ export const LocalDisabled = () => { export const LocalOnly = () => { const props = makeProps(); - props.initAttempt.data.secondFactor = 'off'; props.initAttempt.data.allowPasswordless = false; return ( @@ -121,7 +118,6 @@ export const LocalOnly = () => { export const LocalOnlyWithReasonGatewayCertExpiredWithDbGateway = () => { const props = makeProps(); - props.initAttempt.data.secondFactor = 'off'; props.initAttempt.data.allowPasswordless = false; props.reason = { kind: 'reason.gateway-cert-expired', @@ -138,7 +134,6 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithDbGateway = () => { export const LocalOnlyWithReasonGatewayCertExpiredWithKubeGateway = () => { const props = makeProps(); - props.initAttempt.data.secondFactor = 'off'; props.initAttempt.data.allowPasswordless = false; props.reason = { kind: 'reason.gateway-cert-expired', @@ -155,7 +150,6 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithKubeGateway = () => { export const LocalOnlyWithReasonGatewayCertExpiredWithoutGateway = () => { const props = makeProps(); - props.initAttempt.data.secondFactor = 'off'; props.initAttempt.data.allowPasswordless = false; props.reason = { kind: 'reason.gateway-cert-expired', @@ -172,7 +166,6 @@ export const LocalOnlyWithReasonGatewayCertExpiredWithoutGateway = () => { export const LocalOnlyWithReasonVnetCertExpired = () => { const props = makeProps(); - props.initAttempt.data.secondFactor = 'off'; props.initAttempt.data.allowPasswordless = false; props.reason = { kind: 'reason.vnet-cert-expired', diff --git a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx index 5ceb9d72da53..ef189c5d326d 100644 --- a/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx +++ b/web/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLocal/FormLocal.tsx @@ -16,40 +16,26 @@ * along with this program. If not, see . */ -import React, { useState, useMemo } from 'react'; -import { Flex, ButtonPrimary, Box } from 'design'; +import React, { useState } from 'react'; +import { ButtonPrimary, Box } from 'design'; import Validation, { Validator } from 'shared/components/Validation'; import FieldInput from 'shared/components/FieldInput'; -import { FieldSelect } from 'shared/components/FieldSelect'; -import { - requiredToken, - requiredField, -} from 'shared/components/Validation/rules'; +import { requiredField } from 'shared/components/Validation/rules'; import { useRefAutoFocus } from 'shared/hooks'; -import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions'; import type { Props } from '../FormLogin'; export const FormLocal = ({ - secondFactor, loginAttempt, onLogin, - clearLoginAttempt, hasTransitionEnded, loggedInUserName, autoFocus = false, }: Props & { hasTransitionEnded?: boolean }) => { const [pass, setPass] = useState(''); const [user, setUser] = useState(loggedInUserName || ''); - const [token, setToken] = useState(''); - const mfaOptions = useMemo( - () => createMfaOptions({ auth2faType: secondFactor }), - [] - ); - - const [mfaType, setMfaType] = useState(mfaOptions[0]); const usernameInputRef = useRefAutoFocus({ shouldFocus: hasTransitionEnded && autoFocus && !loggedInUserName, }); @@ -57,13 +43,6 @@ export const FormLocal = ({ shouldFocus: hasTransitionEnded && autoFocus && !!loggedInUserName, }); - function onSetMfaOption(option: MfaOption, validator: Validator) { - setToken(''); - clearLoginAttempt(); - validator.reset(); - setMfaType(option); - } - function onLoginClick( e: React.MouseEvent, validator: Validator @@ -73,7 +52,7 @@ export const FormLocal = ({ return; } - onLogin(user, pass, token, mfaType?.value); + onLogin(user, pass); } const isProcessing = loginAttempt.status === 'processing'; @@ -104,36 +83,6 @@ export const FormLocal = ({ width="100%" disabled={isProcessing} /> - {secondFactor !== 'off' && ( - - onSetMfaOption(opt as MfaOption, validator)} - mr={3} - mb={0} - isDisabled={isProcessing} - /> - {mfaType.value === 'otp' && ( - setToken(e.target.value)} - placeholder="123 456" - mb={0} - disabled={isProcessing} - /> - )} - - )} (); const [initAttempt, init] = useAsync(async () => { - const authSettings = await clustersService.getAuthSettings(clusterUri); - - if (authSettings.preferredMfa === 'u2f') { - throw new Error(`the U2F API for hardware keys is deprecated, \ - please notify your system administrator to update cluster \ - settings to use WebAuthn as the second factor protocol.`); - } - - return authSettings; + return await clustersService.getAuthSettings(clusterUri); }); const [loginAttempt, login, setAttempt] = useAsync( @@ -73,22 +65,13 @@ export default function useClusterLogin(props: Props) { } ); - const onLoginWithLocal = ( - username: string, - password: string, - token: string, - secondFactor?: types.Auth2faType - ) => { - if (secondFactor === 'webauthn') { - setWebauthnLogin({ prompt: 'tap' }); - } - + const onLoginWithLocal = (username: string, password: string) => { + setWebauthnLogin({ prompt: 'tap' }); login({ kind: 'local', clusterUri, username, password, - token, }); }; diff --git a/web/packages/teleterm/src/ui/services/clusters/types.ts b/web/packages/teleterm/src/ui/services/clusters/types.ts index b8deb5905442..4329c4b942ca 100644 --- a/web/packages/teleterm/src/ui/services/clusters/types.ts +++ b/web/packages/teleterm/src/ui/services/clusters/types.ts @@ -21,8 +21,6 @@ import * as shared from 'shared/services/types'; import * as tsh from 'teleterm/services/tshd/types'; import * as uri from 'teleterm/ui/uri'; -export type PreferredMfaType = shared.PreferredMfaType; -export type Auth2faType = shared.Auth2faType; export type AuthProviderType = shared.AuthProviderType; export type AuthType = shared.AuthType; @@ -74,8 +72,6 @@ export type WebauthnLoginCredentialPrompt = { }; export interface AuthSettings extends tsh.AuthSettings { - secondFactor: Auth2faType; - preferredMfa: PreferredMfaType; authType: AuthType; allowPasswordless: boolean; localConnectorName: string;