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
21 changes: 12 additions & 9 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4791,7 +4791,7 @@ func (tc *TeleportClient) RootClusterCACertPool(ctx context.Context) (*x509.Cert
}

// HeadlessApprove handles approval of a headless authentication request.
func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthenticationID string) error {
func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthenticationID string, confirm bool) error {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/HeadlessApprove",
Expand Down Expand Up @@ -4829,15 +4829,18 @@ func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthentic
return trace.Errorf("cannot approve a headless authentication from a non-pending state: %v", headlessAuthn.State.Stringify())
}

confirmationPrompt := fmt.Sprintf("Headless login attempt from IP address %q requires approval.\nContact your administrator if you didn't initiate this login attempt.\nApprove?", headlessAuthn.ClientIpAddress)
ok, err := prompt.Confirmation(ctx, tc.Stdout, prompt.Stdin(), confirmationPrompt)
if err != nil {
return trace.Wrap(err)
}
fmt.Fprintf(tc.Stdout, "Headless login attempt from IP address %q requires approval.\nContact your administrator if you didn't initiate this login attempt.\n", headlessAuthn.ClientIpAddress)

if !ok {
err = rootClient.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED, nil)
return trace.Wrap(err)
if confirm {
ok, err := prompt.Confirmation(ctx, tc.Stdout, prompt.Stdin(), "Approve?")
if err != nil {
return trace.Wrap(err)
}

if !ok {
err = rootClient.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED, nil)
return trace.Wrap(err)
}
}

chall, err := rootClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Expand Down
28 changes: 17 additions & 11 deletions tool/tsh/tsh.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ type CLIConf struct {

// HeadlessAuthenticationID is the ID of a headless authentication.
HeadlessAuthenticationID string

// headlessSkipConfirm determines whether to provide a y/N
// confirmation prompt before prompting for MFA.
headlessSkipConfirm bool
}

// Stdout returns the stdout writer.
Expand Down Expand Up @@ -520,13 +524,14 @@ func main() {
}

const (
authEnvVar = "TELEPORT_AUTH"
clusterEnvVar = "TELEPORT_CLUSTER"
kubeClusterEnvVar = "TELEPORT_KUBE_CLUSTER"
loginEnvVar = "TELEPORT_LOGIN"
bindAddrEnvVar = "TELEPORT_LOGIN_BIND_ADDR"
proxyEnvVar = "TELEPORT_PROXY"
headlessEnvVar = "TELEPORT_HEADLESS"
authEnvVar = "TELEPORT_AUTH"
clusterEnvVar = "TELEPORT_CLUSTER"
kubeClusterEnvVar = "TELEPORT_KUBE_CLUSTER"
loginEnvVar = "TELEPORT_LOGIN"
bindAddrEnvVar = "TELEPORT_LOGIN_BIND_ADDR"
proxyEnvVar = "TELEPORT_PROXY"
headlessEnvVar = "TELEPORT_HEADLESS"
headlessSkipConfirmEnvVar = "TELEPORT_HEADLESS_SKIP_CONFIRM"
// TELEPORT_SITE uses the older deprecated "site" terminology to refer to a
// cluster. All new code should use TELEPORT_CLUSTER instead.
siteEnvVar = "TELEPORT_SITE"
Expand Down Expand Up @@ -970,8 +975,9 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error {

// Headless login approval
headless := app.Command("headless", "Headless authentication commands.").Interspersed(true)
approve := headless.Command("approve", "Approve a headless authentication request.").Interspersed(true)
approve.Arg("request id", "Headless authentication request ID").StringVar(&cf.HeadlessAuthenticationID)
headlessApprove := headless.Command("approve", "Approve a headless authentication request.").Interspersed(true)
headlessApprove.Arg("request id", "Headless authentication request ID").StringVar(&cf.HeadlessAuthenticationID)
headlessApprove.Flag("skip-confirm", "Skip confirmation and prompt for MFA immediately").Envar(headlessSkipConfirmEnvVar).BoolVar(&cf.headlessSkipConfirm)

reqDrop := req.Command("drop", "Drop one more access requests from current identity.")
reqDrop.Arg("request-id", "IDs of requests to drop (default drops all requests)").Default("*").StringsVar(&cf.RequestIDs)
Expand Down Expand Up @@ -1326,7 +1332,7 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error {
case kubectl.FullCommand():
idx := slices.Index(args, kubectl.FullCommand())
err = onKubectlCommand(&cf, args, args[idx:])
case approve.FullCommand():
case headlessApprove.FullCommand():
err = onHeadlessApprove(&cf)
default:
// Handle commands that might not be available.
Expand Down Expand Up @@ -4831,7 +4837,7 @@ func onHeadlessApprove(cf *CLIConf) error {

tc.Stdin = os.Stdin
err = client.RetryWithRelogin(cf.Context, tc, func() error {
return tc.HeadlessApprove(cf.Context, cf.HeadlessAuthenticationID)
return tc.HeadlessApprove(cf.Context, cf.HeadlessAuthenticationID, !cf.headlessSkipConfirm)
})
return trace.Wrap(err)
}
Expand Down