From 9aaabf656e0aecd293fedc60d283ba213f673499 Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Wed, 17 May 2023 18:27:46 -0400 Subject: [PATCH 1/8] Initial support for MFA in Assist --- lib/web/apiserver.go | 10 ++-------- lib/web/command.go | 11 +---------- lib/web/desktop.go | 2 +- lib/web/terminal.go | 41 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 80e527194b8a2..42240bb442d83 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -1185,19 +1185,13 @@ func (h *Handler) ping(w http.ResponseWriter, r *http.Request, p httprouter.Para // TODO: This part should be removed once the plugin support is added to OSS. if proxyConfig.AssistEnabled { - // TODO(jakule): Currently assist is disabled when per-session MFA is enabled as this part is not implemented. - authPreference, err := h.cfg.ProxyClient.GetAuthPreference(r.Context()) - if err != nil { - return webclient.AuthenticationSettings{}, trace.Wrap(err) - } - mfaRequired := authPreference.GetRequireMFAType() != types.RequireMFAType_OFF enabled, err := h.cfg.ProxyClient.IsAssistEnabled(r.Context()) if err != nil { return webclient.AuthenticationSettings{}, trace.Wrap(err) } - // disable if per-session MFA is enabled and it's ok by the auth - proxyConfig.AssistEnabled = enabled.Enabled && !mfaRequired + // disable if auth doesn't support assist + proxyConfig.AssistEnabled = enabled.Enabled } pr, err := h.cfg.ProxyClient.Ping(r.Context()) diff --git a/lib/web/command.go b/lib/web/command.go index 07d1cebb302b2..4b55290359f33 100644 --- a/lib/web/command.go +++ b/lib/web/command.go @@ -46,9 +46,7 @@ import ( "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/proxy" "github.com/gravitational/teleport/lib/reversetunnel" - "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/session" - "github.com/gravitational/teleport/lib/teleagent" ) // CommandRequest is a request to execute a command on all nodes that match the query. @@ -447,14 +445,7 @@ func (t *commandHandler) streamOutput(ctx context.Context, tc *client.TeleportCl ctx, span := t.tracer.Start(ctx, "commandHandler/streamOutput") defer span.End() - mfaAuth := func(ctx context.Context, ws WSConn, tc *client.TeleportClient, - accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator, - ) (*client.NodeClient, error) { - return nil, trace.NotImplemented("MFA is not supported for command execution") - } - - //TODO(jakule): Implement MFA support - nc, err := t.connectToHost(ctx, t.ws, tc, mfaAuth) + nc, err := t.connectToHost(ctx, t.ws, tc, t.connectToNodeWithMFA) if err != nil { t.log.WithError(err).Warn("Unable to stream terminal - failure connecting to host") t.writeError(err) diff --git a/lib/web/desktop.go b/lib/web/desktop.go index 66de7829fa3c2..69afce3bc3eb4 100644 --- a/lib/web/desktop.go +++ b/lib/web/desktop.go @@ -264,7 +264,7 @@ func desktopTLSConfig(ctx context.Context, ws *websocket.Conn, pc *client.ProxyC TLSCert: sessCtx.cfg.Session.GetTLSCert(), WindowsDesktopCerts: make(map[string][]byte), }, - }, promptMFAChallenge(stream, tdpMFACodec{})) + }, promptMFAChallenge(&stream.WSStream, tdpMFACodec{})) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/terminal.go b/lib/web/terminal.go index 9a334e5361f62..10b0e65e0070e 100644 --- a/lib/web/terminal.go +++ b/lib/web/terminal.go @@ -467,7 +467,7 @@ func (t *TerminalHandler) makeClient(ctx context.Context, ws *websocket.Conn) (* // used to access nodes which require per-session mfa. The ceremony is performed directly // to make use of the authProvider already established for the session instead of leveraging // the TeleportClient which would require dialing the auth server a second time. -func (t *TerminalHandler) issueSessionMFACerts(ctx context.Context, tc *client.TeleportClient) ([]ssh.AuthMethod, error) { +func (t *sshBaseHandler) issueSessionMFACerts(ctx context.Context, tc *client.TeleportClient, wsStream *WSStream) ([]ssh.AuthMethod, error) { ctx, span := t.tracer.Start(ctx, "terminal/issueSessionMFACerts") defer span.End() @@ -550,7 +550,7 @@ func (t *TerminalHandler) issueSessionMFACerts(ctx context.Context, tc *client.T } span.AddEvent("prompting user with mfa challenge") - assertion, err := promptMFAChallenge(t.stream, protobufMFACodec{})(ctx, tc.WebProxyAddr, challenge) + assertion, err := promptMFAChallenge(wsStream, protobufMFACodec{})(ctx, tc.WebProxyAddr, challenge) if err != nil { return nil, trace.Wrap(err) } @@ -589,7 +589,7 @@ func (t *TerminalHandler) issueSessionMFACerts(ctx context.Context, tc *client.T } func promptMFAChallenge( - stream *TerminalStream, + stream *WSStream, codec mfaCodec, ) client.PromptMFAChallengeHandler { return func(ctx context.Context, proxyAddr string, c *authproto.MFAAuthenticateChallenge) (*authproto.MFAAuthenticateResponse, error) { @@ -783,7 +783,40 @@ func (t *sshBaseHandler) connectToNode(ctx context.Context, ws WSConn, tc *clien // host with the retrieved single use certs. func (t *TerminalHandler) connectToNodeWithMFA(ctx context.Context, ws WSConn, tc *client.TeleportClient, accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator) (*client.NodeClient, error) { // perform mfa ceremony and retrieve new certs - authMethods, err := t.issueSessionMFACerts(ctx, tc) + authMethods, err := t.issueSessionMFACerts(ctx, tc, &t.stream.WSStream) + if err != nil { + return nil, trace.Wrap(err) + } + + sshConfig := &ssh.ClientConfig{ + User: tc.HostLogin, + Auth: authMethods, + HostKeyCallback: tc.HostKeyCallback, + } + + // connect to the node again with the new certs + conn, _, err := t.router.DialHost(ctx, ws.RemoteAddr(), ws.LocalAddr(), t.sessionData.ServerID, strconv.Itoa(t.sessionData.ServerHostPort), tc.SiteName, accessChecker, getAgent, signer) + if err != nil { + return nil, trace.Wrap(err) + } + + nc, err := client.NewNodeClient(ctx, sshConfig, conn, + net.JoinHostPort(t.sessionData.ServerID, strconv.Itoa(t.sessionData.ServerHostPort)), + t.sessionData.ServerHostname, + tc, modules.GetModules().IsBoringBinary()) + if err != nil { + return nil, trace.NewAggregate(err, conn.Close()) + } + + return nc, nil +} + +// connectToNodeWithMFA attempts to perform the mfa ceremony and then dial the +// host with the retrieved single use certs. +// TODO(jakule): remove duplication between this and the above connectToNodeWithMFA +func (t *commandHandler) connectToNodeWithMFA(ctx context.Context, ws WSConn, tc *client.TeleportClient, accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator) (*client.NodeClient, error) { + // perform mfa ceremony and retrieve new certs + authMethods, err := t.issueSessionMFACerts(ctx, tc, t.stream) if err != nil { return nil, trace.Wrap(err) } From 43f49377b00f63d358485973c6b998973b65df13 Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Thu, 18 May 2023 13:42:41 -0400 Subject: [PATCH 2/8] UI webauth handler --- .../src/Assist/Chat/ChatItem/Action/RunAction.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx index fc0b5ff3adf07..01970537180b3 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx @@ -27,6 +27,7 @@ import { ExecuteRemoteCommandContent } from 'teleport/Assist/services/messages'; import { MessageTypeEnum, Protobuf } from 'teleport/lib/term/protobuf'; import { Dots } from 'teleport/Assist/Dots'; import cfg from 'teleport/config'; +import {makeMfaAuthenticateChallenge} from "teleport/services/auth"; interface RunCommandProps { actions: ExecuteRemoteCommandContent; @@ -136,6 +137,17 @@ export function RunCommand(props: RunCommandProps) { return s; }); + break; + case MessageTypeEnum.ERROR: + console.error(msg.payload); + break; + case MessageTypeEnum.WEBAUTHN_CHALLENGE: + //TODO: handle webauthn challenge + console.log(msg.payload); + const challengeJson = msg.payload; + const challenge = JSON.parse(challengeJson); + const publicKey = makeMfaAuthenticateChallenge(challenge).webauthnPublicKey; + break; } }; From ecc036e1700619ef875e4d6fece7547979439da4 Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Tue, 23 May 2023 00:28:24 -0400 Subject: [PATCH 3/8] WebUI - WIP --- .../Assist/Chat/ChatItem/Action/RunAction.tsx | 122 +++++++++++++----- 1 file changed, 91 insertions(+), 31 deletions(-) diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx index 01970537180b3..b5f99c88e2bdd 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx @@ -27,7 +27,14 @@ import { ExecuteRemoteCommandContent } from 'teleport/Assist/services/messages'; import { MessageTypeEnum, Protobuf } from 'teleport/lib/term/protobuf'; import { Dots } from 'teleport/Assist/Dots'; import cfg from 'teleport/config'; -import {makeMfaAuthenticateChallenge} from "teleport/services/auth"; +import { + WebauthnAssertionResponse, +} from 'teleport/services/auth'; +import useWebAuthn from 'teleport/lib/useWebAuthn'; +import { EventEmitterWebAuthnSender } from 'teleport/lib/EventEmitterWebAuthnSender'; +import { Message, MfaJson } from 'teleport/lib/tdp/codec'; +import AuthnDialog from "teleport/components/AuthnDialog"; +import {TermEvent} from "teleport/lib/term/enums"; interface RunCommandProps { actions: ExecuteRemoteCommandContent; @@ -71,37 +78,31 @@ interface RawPayload { payload: string; } -export function RunCommand(props: RunCommandProps) { - const { clusterId } = useStickyClusterId(); - const urlParams = useParams<{ conversationId: string }>(); +class assistClient extends EventEmitterWebAuthnSender { + private ws: WebSocket; + private proto: Protobuf; + encoder = new window.TextEncoder(); - const [state, setState] = useState(() => []); + constructor(url: string, setState: React.Dispatch>) { + super(); - const params = convertContentToCommand(props.actions); + this.proto = new Protobuf(); - const execParams = { - ...params, - conversation_id: urlParams.conversationId, - execution_id: crypto.randomUUID(), - }; + const refWS = useRef(); - const url = cfg.getAssistExecuteCommandUrl( - getHostName(), - clusterId, - getAccessToken(), - execParams - ); + React.useEffect(() => { + if(refWS.current) { + this.ws = refWS.current; + return; + } - const websocket = useRef(null); - const protoRef = useRef(null); + this.ws = new WebSocket(url); + this.ws.binaryType = 'arraybuffer'; + refWS.current = this.ws; - useEffect(() => { - if (!websocket.current) { const proto = new Protobuf(); - const ws = new WebSocket(url); - ws.binaryType = 'arraybuffer'; - ws.onmessage = event => { + this.ws.onmessage = event => { const uintArray = new Uint8Array(event.data); const msg = proto.decode(uintArray); @@ -144,24 +145,83 @@ export function RunCommand(props: RunCommandProps) { case MessageTypeEnum.WEBAUTHN_CHALLENGE: //TODO: handle webauthn challenge console.log(msg.payload); - const challengeJson = msg.payload; - const challenge = JSON.parse(challengeJson); - const publicKey = makeMfaAuthenticateChallenge(challenge).webauthnPublicKey; + + this.emit(TermEvent.WEBAUTHN_CHALLENGE, msg.payload); break; } }; + }, [refWS.current]); + } + + sendWebAuthn(data: WebauthnAssertionResponse) { + console.log("sendWebAuthn", data); + const msg = this.encodeMfaJson({ + mfaType: 'n', + jsonString: JSON.stringify(data), + }); + this.send(msg); + } - protoRef.current = proto; - websocket.current = ws; + send(data) { + if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !data) { + console.log('websocket unavailable', this.ws, data); + return; } - }, []); + + console.log("send", data); + const msg = this.proto.encodeRawMessage(data); + const bytearray = new Uint8Array(msg); + this.ws.send(bytearray.buffer); + } + + // | message type (10) | mfa_type byte | message_length uint32 | json []byte + encodeMfaJson(mfaJson: MfaJson): Message { + const dataUtf8array = this.encoder.encode(mfaJson.jsonString); + return dataUtf8array + } +} + +export function RunCommand(props: RunCommandProps) { + const { clusterId } = useStickyClusterId(); + const urlParams = useParams<{ conversationId: string }>(); + + const [state, setState] = useState(() => []); + + const params = convertContentToCommand(props.actions); + + const execParams = { + ...params, + conversation_id: urlParams.conversationId, + execution_id: crypto.randomUUID(), + }; + + const url = cfg.getAssistExecuteCommandUrl( + getHostName(), + clusterId, + getAccessToken(), + execParams + ); + + const assistClt = new assistClient(url, setState); + const webauthn = useWebAuthn(assistClt); const nodes = state.map((item, index) => ( )); - return
{nodes}
; + return ( + <> + {webauthn.requested && ( + {} } + errorText={webauthn.errorText} + /> + )} +
{nodes}
+ + ); } interface NodeOutputProps { From 59d2f4e3ee358e639d67ea84ba2b8953e06aae5d Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Wed, 24 May 2023 14:21:21 -0400 Subject: [PATCH 4/8] Run prettier --- .../Assist/Chat/ChatItem/Action/RunAction.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx index b5f99c88e2bdd..ca6564035f083 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx @@ -27,14 +27,12 @@ import { ExecuteRemoteCommandContent } from 'teleport/Assist/services/messages'; import { MessageTypeEnum, Protobuf } from 'teleport/lib/term/protobuf'; import { Dots } from 'teleport/Assist/Dots'; import cfg from 'teleport/config'; -import { - WebauthnAssertionResponse, -} from 'teleport/services/auth'; +import { WebauthnAssertionResponse } from 'teleport/services/auth'; import useWebAuthn from 'teleport/lib/useWebAuthn'; import { EventEmitterWebAuthnSender } from 'teleport/lib/EventEmitterWebAuthnSender'; import { Message, MfaJson } from 'teleport/lib/tdp/codec'; -import AuthnDialog from "teleport/components/AuthnDialog"; -import {TermEvent} from "teleport/lib/term/enums"; +import AuthnDialog from 'teleport/components/AuthnDialog'; +import { TermEvent } from 'teleport/lib/term/enums'; interface RunCommandProps { actions: ExecuteRemoteCommandContent; @@ -83,7 +81,10 @@ class assistClient extends EventEmitterWebAuthnSender { private proto: Protobuf; encoder = new window.TextEncoder(); - constructor(url: string, setState: React.Dispatch>) { + constructor( + url: string, + setState: React.Dispatch> + ) { super(); this.proto = new Protobuf(); @@ -91,7 +92,7 @@ class assistClient extends EventEmitterWebAuthnSender { const refWS = useRef(); React.useEffect(() => { - if(refWS.current) { + if (refWS.current) { this.ws = refWS.current; return; } @@ -155,7 +156,7 @@ class assistClient extends EventEmitterWebAuthnSender { } sendWebAuthn(data: WebauthnAssertionResponse) { - console.log("sendWebAuthn", data); + console.log('sendWebAuthn', data); const msg = this.encodeMfaJson({ mfaType: 'n', jsonString: JSON.stringify(data), @@ -169,7 +170,7 @@ class assistClient extends EventEmitterWebAuthnSender { return; } - console.log("send", data); + console.log('send', data); const msg = this.proto.encodeRawMessage(data); const bytearray = new Uint8Array(msg); this.ws.send(bytearray.buffer); @@ -178,7 +179,7 @@ class assistClient extends EventEmitterWebAuthnSender { // | message type (10) | mfa_type byte | message_length uint32 | json []byte encodeMfaJson(mfaJson: MfaJson): Message { const dataUtf8array = this.encoder.encode(mfaJson.jsonString); - return dataUtf8array + return dataUtf8array; } } @@ -215,7 +216,7 @@ export function RunCommand(props: RunCommandProps) { {webauthn.requested && ( {} } + onCancel={() => {}} errorText={webauthn.errorText} /> )} From ac6e662889157e867097ffcfff9f1277b7f503fa Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Wed, 24 May 2023 22:51:44 -0400 Subject: [PATCH 5/8] Perform MFA ceremony only once. --- lib/web/command.go | 64 +++++++++++++++++++++++++++++++++++++++++++++ lib/web/terminal.go | 35 +++---------------------- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/lib/web/command.go b/lib/web/command.go index 4b55290359f33..ba47b4dac654f 100644 --- a/lib/web/command.go +++ b/lib/web/command.go @@ -25,6 +25,7 @@ import ( "fmt" "net/http" "strings" + "sync" "time" "github.com/gogo/protobuf/proto" @@ -33,6 +34,7 @@ import ( "github.com/julienschmidt/httprouter" "github.com/sirupsen/logrus" oteltrace "go.opentelemetry.io/otel/trace" + "golang.org/x/crypto/ssh" "google.golang.org/protobuf/types/known/timestamppb" "github.com/gravitational/teleport" @@ -46,7 +48,9 @@ import ( "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/proxy" "github.com/gravitational/teleport/lib/reversetunnel" + "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/session" + "github.com/gravitational/teleport/lib/teleagent" ) // CommandRequest is a request to execute a command on all nodes that match the query. @@ -183,6 +187,8 @@ func (h *Handler) executeCommand( h.log.Debugf("Found %d hosts to run Assist command %q on.", len(hosts), req.Command) + mfaCacheFn := getMFACacheFn() + for _, host := range hosts { err := func() error { sessionData, err := h.generateCommandSession(&host, req.Login, clusterName, sessionCtx.cfg.User) @@ -204,6 +210,7 @@ func (h *Handler) executeCommand( Router: h.cfg.Router, TracerProvider: h.cfg.TracerProvider, LocalAuthProvider: h.auth.accessPoint, + mfaFuncCache: mfaCacheFn, } handler, err := newCommandHandler(ctx, commandHandlerConfig) @@ -255,6 +262,27 @@ func (h *Handler) executeCommand( return nil, nil } +// getMFACacheFn returns a function that caches the result of the given +// get function. The cache is protected by a mutex, so it is safe to call +// the returned function from multiple goroutines. +func getMFACacheFn() mfaFuncCache { + var mutex sync.Mutex + var authMethods []ssh.AuthMethod + + return func(issueMfaAuthFn func() ([]ssh.AuthMethod, error)) ([]ssh.AuthMethod, error) { + mutex.Lock() + defer mutex.Unlock() + + if authMethods != nil { + return authMethods, nil + } + + var err error + authMethods, err = issueMfaAuthFn() + return authMethods, trace.Wrap(err) + } +} + func newCommandHandler(ctx context.Context, cfg CommandHandlerConfig) (*commandHandler, error) { err := cfg.CheckAndSetDefaults() if err != nil { @@ -280,6 +308,7 @@ func newCommandHandler(ctx context.Context, cfg CommandHandlerConfig) (*commandH localAuthProvider: cfg.LocalAuthProvider, tracer: cfg.tracer, }, + mfaAuthCache: cfg.mfaFuncCache, }, nil } @@ -308,6 +337,8 @@ type CommandHandlerConfig struct { LocalAuthProvider agentless.AuthProvider // tracer is used to create spans tracer oteltrace.Tracer + // mfaFuncCache is used to cache the MFA auth method + mfaFuncCache mfaFuncCache } // CheckAndSetDefaults checks and sets default values. @@ -346,11 +377,19 @@ func (t *CommandHandlerConfig) CheckAndSetDefaults() error { return trace.BadParameter("LocalAuthProvider must be provided") } + if t.mfaFuncCache == nil { + return trace.BadParameter("mfaFuncCache must be provided") + } + t.tracer = t.TracerProvider.Tracer("webcommand") return nil } +// mfaFuncCache is a function type that caches the result of a function that +// returns a list of ssh.AuthMethods. +type mfaFuncCache func(func() ([]ssh.AuthMethod, error)) ([]ssh.AuthMethod, error) + // commandHandler is a handler for executing commands on a remote node. type commandHandler struct { sshBaseHandler @@ -360,6 +399,11 @@ type commandHandler struct { // ws a raw websocket connection to the client. ws WSConn + + // mfaAuthCache is a function that caches the result of a function that + // returns a list of ssh.AuthMethods. It is used to cache the result of + // the MFA challenge. + mfaAuthCache mfaFuncCache } // sendError sends an error message to the client using the provided websocket. @@ -473,6 +517,26 @@ func (t *commandHandler) streamOutput(ctx context.Context, tc *client.TeleportCl t.log.Debug("Sent close event to web client.") } +// connectToNodeWithMFA attempts to perform the mfa ceremony and then dial the +// host with the retrieved single use certs. +// If called multiple times, the mfa ceremony will only be performed once. +func (t *commandHandler) connectToNodeWithMFA(ctx context.Context, ws WSConn, tc *client.TeleportClient, accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator) (*client.NodeClient, error) { + authMethods, err := t.mfaAuthCache(func() ([]ssh.AuthMethod, error) { + // perform mfa ceremony and retrieve new certs + authMethods, err := t.issueSessionMFACerts(ctx, tc, t.stream) + if err != nil { + return nil, trace.Wrap(err) + } + + return authMethods, nil + }) + if err != nil { + return nil, trace.Wrap(err) + } + + return t.connectToNodeWithMFABase(ctx, ws, tc, accessChecker, getAgent, signer, authMethods) +} + // Close is no-op as we never want to close the connection to the client. // Connection should be closed in the handler when it was created. func (t *commandHandler) Close() error { diff --git a/lib/web/terminal.go b/lib/web/terminal.go index 10b0e65e0070e..d87f898d24acb 100644 --- a/lib/web/terminal.go +++ b/lib/web/terminal.go @@ -788,39 +788,12 @@ func (t *TerminalHandler) connectToNodeWithMFA(ctx context.Context, ws WSConn, t return nil, trace.Wrap(err) } - sshConfig := &ssh.ClientConfig{ - User: tc.HostLogin, - Auth: authMethods, - HostKeyCallback: tc.HostKeyCallback, - } - - // connect to the node again with the new certs - conn, _, err := t.router.DialHost(ctx, ws.RemoteAddr(), ws.LocalAddr(), t.sessionData.ServerID, strconv.Itoa(t.sessionData.ServerHostPort), tc.SiteName, accessChecker, getAgent, signer) - if err != nil { - return nil, trace.Wrap(err) - } - - nc, err := client.NewNodeClient(ctx, sshConfig, conn, - net.JoinHostPort(t.sessionData.ServerID, strconv.Itoa(t.sessionData.ServerHostPort)), - t.sessionData.ServerHostname, - tc, modules.GetModules().IsBoringBinary()) - if err != nil { - return nil, trace.NewAggregate(err, conn.Close()) - } - - return nc, nil + return t.connectToNodeWithMFABase(ctx, ws, tc, accessChecker, getAgent, signer, authMethods) } -// connectToNodeWithMFA attempts to perform the mfa ceremony and then dial the -// host with the retrieved single use certs. -// TODO(jakule): remove duplication between this and the above connectToNodeWithMFA -func (t *commandHandler) connectToNodeWithMFA(ctx context.Context, ws WSConn, tc *client.TeleportClient, accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator) (*client.NodeClient, error) { - // perform mfa ceremony and retrieve new certs - authMethods, err := t.issueSessionMFACerts(ctx, tc, t.stream) - if err != nil { - return nil, trace.Wrap(err) - } - +// connectToNodeWithMFABase attempts to dial the host with the provided auth +// methods. +func (t *sshBaseHandler) connectToNodeWithMFABase(ctx context.Context, ws WSConn, tc *client.TeleportClient, accessChecker services.AccessChecker, getAgent teleagent.Getter, signer agentless.SignerCreator, authMethods []ssh.AuthMethod) (*client.NodeClient, error) { sshConfig := &ssh.ClientConfig{ User: tc.HostLogin, Auth: authMethods, From fba4809a6f029f5b8a450b6b2ec4c86ebc7484f7 Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Wed, 24 May 2023 23:25:50 -0400 Subject: [PATCH 6/8] Cleanup JS --- .../Assist/Chat/ChatItem/Action/RunAction.tsx | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx index ca6564035f083..4bc5322e912d7 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router'; @@ -30,7 +30,6 @@ import cfg from 'teleport/config'; import { WebauthnAssertionResponse } from 'teleport/services/auth'; import useWebAuthn from 'teleport/lib/useWebAuthn'; import { EventEmitterWebAuthnSender } from 'teleport/lib/EventEmitterWebAuthnSender'; -import { Message, MfaJson } from 'teleport/lib/tdp/codec'; import AuthnDialog from 'teleport/components/AuthnDialog'; import { TermEvent } from 'teleport/lib/term/enums'; @@ -78,8 +77,8 @@ interface RawPayload { class assistClient extends EventEmitterWebAuthnSender { private ws: WebSocket; - private proto: Protobuf; - encoder = new window.TextEncoder(); + readonly proto: Protobuf = new Protobuf(); + readonly encoder = new window.TextEncoder(); constructor( url: string, @@ -87,8 +86,6 @@ class assistClient extends EventEmitterWebAuthnSender { ) { super(); - this.proto = new Protobuf(); - const refWS = useRef(); React.useEffect(() => { @@ -101,11 +98,9 @@ class assistClient extends EventEmitterWebAuthnSender { this.ws.binaryType = 'arraybuffer'; refWS.current = this.ws; - const proto = new Protobuf(); - this.ws.onmessage = event => { const uintArray = new Uint8Array(event.data); - const msg = proto.decode(uintArray); + const msg = this.proto.decode(uintArray); switch (msg.type) { case MessageTypeEnum.RAW: @@ -121,7 +116,7 @@ class assistClient extends EventEmitterWebAuthnSender { }); } - const s = state.map(item => { + return state.map(item => { if (item.nodeId === data.node_id) { if (!item.stdout) { item.stdout = ''; @@ -135,8 +130,6 @@ class assistClient extends EventEmitterWebAuthnSender { return item; }); - - return s; }); break; @@ -144,11 +137,7 @@ class assistClient extends EventEmitterWebAuthnSender { console.error(msg.payload); break; case MessageTypeEnum.WEBAUTHN_CHALLENGE: - //TODO: handle webauthn challenge - console.log(msg.payload); - this.emit(TermEvent.WEBAUTHN_CHALLENGE, msg.payload); - break; } }; @@ -156,31 +145,20 @@ class assistClient extends EventEmitterWebAuthnSender { } sendWebAuthn(data: WebauthnAssertionResponse) { - console.log('sendWebAuthn', data); - const msg = this.encodeMfaJson({ - mfaType: 'n', - jsonString: JSON.stringify(data), - }); + const msg = this.encoder.encode(JSON.stringify(data)); this.send(msg); } send(data) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN || !data) { - console.log('websocket unavailable', this.ws, data); + console.warn('websocket unavailable', this.ws, data); return; } - console.log('send', data); const msg = this.proto.encodeRawMessage(data); const bytearray = new Uint8Array(msg); this.ws.send(bytearray.buffer); } - - // | message type (10) | mfa_type byte | message_length uint32 | json []byte - encodeMfaJson(mfaJson: MfaJson): Message { - const dataUtf8array = this.encoder.encode(mfaJson.jsonString); - return dataUtf8array; - } } export function RunCommand(props: RunCommandProps) { From af28dc72a53a0c75282175314f5bcd37493846ed Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Thu, 25 May 2023 11:04:02 -0400 Subject: [PATCH 7/8] Remove hacky WS logic --- .../Assist/Chat/ChatItem/Action/RunAction.tsx | 96 +++++++++---------- 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx index 4bc5322e912d7..cd6ad17ce895f 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import React, { useRef, useState } from 'react'; +import React, { useState } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router'; @@ -76,7 +76,7 @@ interface RawPayload { } class assistClient extends EventEmitterWebAuthnSender { - private ws: WebSocket; + private readonly ws: WebSocket; readonly proto: Protobuf = new Protobuf(); readonly encoder = new window.TextEncoder(); @@ -86,62 +86,52 @@ class assistClient extends EventEmitterWebAuthnSender { ) { super(); - const refWS = useRef(); + this.ws = new WebSocket(url); + this.ws.binaryType = 'arraybuffer'; - React.useEffect(() => { - if (refWS.current) { - this.ws = refWS.current; - return; - } + this.ws.onmessage = event => { + const uintArray = new Uint8Array(event.data); + const msg = this.proto.decode(uintArray); - this.ws = new WebSocket(url); - this.ws.binaryType = 'arraybuffer'; - refWS.current = this.ws; - - this.ws.onmessage = event => { - const uintArray = new Uint8Array(event.data); - const msg = this.proto.decode(uintArray); - - switch (msg.type) { - case MessageTypeEnum.RAW: - const data = JSON.parse(msg.payload) as RawPayload; - const payload = atob(data.payload); - - setState(state => { - const results = state.find(node => node.nodeId == data.node_id); - if (!results) { - state.push({ - nodeId: data.node_id, - status: RunActionStatus.Connecting, - }); - } + switch (msg.type) { + case MessageTypeEnum.RAW: + const data = JSON.parse(msg.payload) as RawPayload; + const payload = atob(data.payload); + + setState(state => { + const results = state.find(node => node.nodeId == data.node_id); + if (!results) { + state.push({ + nodeId: data.node_id, + status: RunActionStatus.Connecting, + }); + } - return state.map(item => { - if (item.nodeId === data.node_id) { - if (!item.stdout) { - item.stdout = ''; - } - return { - ...item, - status: RunActionStatus.Finished, - stdout: item.stdout + payload, - }; + return state.map(item => { + if (item.nodeId === data.node_id) { + if (!item.stdout) { + item.stdout = ''; } + return { + ...item, + status: RunActionStatus.Finished, + stdout: item.stdout + payload, + }; + } - return item; - }); + return item; }); - - break; - case MessageTypeEnum.ERROR: - console.error(msg.payload); - break; - case MessageTypeEnum.WEBAUTHN_CHALLENGE: - this.emit(TermEvent.WEBAUTHN_CHALLENGE, msg.payload); - break; - } - }; - }, [refWS.current]); + }); + + break; + case MessageTypeEnum.ERROR: + console.error(msg.payload); + break; + case MessageTypeEnum.WEBAUTHN_CHALLENGE: + this.emit(TermEvent.WEBAUTHN_CHALLENGE, msg.payload); + break; + } + }; } sendWebAuthn(data: WebauthnAssertionResponse) { @@ -182,7 +172,7 @@ export function RunCommand(props: RunCommandProps) { execParams ); - const assistClt = new assistClient(url, setState); + const [assistClt] = useState(() => new assistClient(url, setState)); const webauthn = useWebAuthn(assistClt); const nodes = state.map((item, index) => ( From 8077d70a6858930b9cad2f40b3d4f10e4e22ef2d Mon Sep 17 00:00:00 2001 From: Jakub Nyckowski Date: Thu, 25 May 2023 11:17:26 -0400 Subject: [PATCH 8/8] Add cancel MFA logic --- .../src/Assist/Chat/ChatItem/Action/RunAction.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx index cd6ad17ce895f..f12a2fbaadd1d 100644 --- a/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx +++ b/web/packages/teleport/src/Assist/Chat/ChatItem/Action/RunAction.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router'; @@ -175,6 +175,15 @@ export function RunCommand(props: RunCommandProps) { const [assistClt] = useState(() => new assistClient(url, setState)); const webauthn = useWebAuthn(assistClt); + const cancelCallback = useCallback(() => { + webauthn.setState(prevState => { + return { + ...prevState, + requested: false, + }; + }); + }, [webauthn]); + const nodes = state.map((item, index) => ( )); @@ -184,7 +193,7 @@ export function RunCommand(props: RunCommandProps) { {webauthn.requested && ( {}} + onCancel={cancelCallback} errorText={webauthn.errorText} /> )}