From e6ba11a0d91a2545b871dc201f0617afd080105e Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 2 Mar 2026 09:12:36 -0500 Subject: [PATCH 1/3] fix(auth): resolve MFA option by label, type, or display string The --mfa-option-id flag was passed directly to the API without validation. Users who provided the MFA option label (e.g. "Get a text") instead of the type (e.g. "sms") would silently fail, causing the flow to loop back to the MFA selection screen. Now the CLI fetches the connection's available MFA options and resolves the user's input against label, type, or the combined display string, always sending the correct type to the API. Unknown options produce a clear error listing available choices. Co-Authored-By: Claude Opus 4.6 --- cmd/auth_connections.go | 28 +++++++++ cmd/auth_connections_test.go | 118 +++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index bc8f9a7..4ad9897 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -450,6 +450,34 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn return fmt.Errorf("must provide at least one of: --field, --mfa-option-id, or --sso-button-selector") } + // Resolve MFA option: the user may pass the label (e.g. "Get a text"), the + // type (e.g. "sms"), or the display string ("Get a text (sms)"). The API + // expects the type, so look up the connection's available options and map + // whatever the user provided to the correct type value. + if hasMfaOption { + conn, err := c.svc.Get(ctx, in.ID) + if err == nil && len(conn.MfaOptions) > 0 { + resolved := false + for _, opt := range conn.MfaOptions { + displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type) + if strings.EqualFold(in.MfaOptionID, opt.Type) || + strings.EqualFold(in.MfaOptionID, opt.Label) || + strings.EqualFold(in.MfaOptionID, displayName) { + in.MfaOptionID = opt.Type + resolved = true + break + } + } + if !resolved { + available := make([]string, 0, len(conn.MfaOptions)) + for _, opt := range conn.MfaOptions { + available = append(available, fmt.Sprintf("%s (%s)", opt.Label, opt.Type)) + } + return fmt.Errorf("unknown MFA option %q; available: %s", in.MfaOptionID, strings.Join(available, ", ")) + } + } + } + params := kernel.AuthConnectionSubmitParams{ SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{ Fields: in.FieldValues, diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index 41bcb28..4f12f2d 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -196,3 +196,121 @@ func TestAuthConnectionsList_JSONOutput_PrintsRawResponse(t *testing.T) { assert.Contains(t, out, "\"profile_name\"") assert.Contains(t, out, "\"raf-leaseweb\"") } + +func newFakeWithMfaOptions(options []kernel.ManagedAuthMfaOption) *FakeAuthConnectionService { + return &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return &kernel.ManagedAuth{ + ID: id, + MfaOptions: options, + }, nil + }, + SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + }, + } +} + +func TestSubmit_MfaOptionResolvesType(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + {Label: "Have us call you", Type: "call"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "sms", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionResolvesLabel(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + {Label: "Have us call you", Type: "call"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "Get a text", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionResolvesDisplayString(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "Get a text (sms)", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionResolvesLabelCaseInsensitive(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + }) + + var submittedID string + fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) { + submittedID = body.SubmitFieldsRequest.MfaOptionID.Value + return &kernel.SubmitFieldsResponse{Accepted: true}, nil + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "get a TEXT", + Output: "json", + }) + require.NoError(t, err) + assert.Equal(t, "sms", submittedID) +} + +func TestSubmit_MfaOptionRejectsUnknown(t *testing.T) { + fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ + {Label: "Get a text", Type: "sms"}, + {Label: "Have us call you", Type: "call"}, + }) + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "carrier pigeon", + Output: "json", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown MFA option") + assert.Contains(t, err.Error(), "carrier pigeon") + assert.Contains(t, err.Error(), "Get a text (sms)") +} From 53d528b86dba74e5a5634a828c3f269ab295d667 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 2 Mar 2026 09:44:31 -0500 Subject: [PATCH 2/3] fix: surface Get error instead of silently skipping MFA resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If fetching the connection fails (network error, auth issue, etc.), the MFA resolution was silently skipped, sending the raw user input to the API — reproducing the exact bug this PR fixes. Now the error is returned immediately. Co-Authored-By: Claude Opus 4.6 --- cmd/auth_connections.go | 5 ++++- cmd/auth_connections_test.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 4ad9897..4f367ec 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -456,7 +456,10 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn // whatever the user provided to the correct type value. if hasMfaOption { conn, err := c.svc.Get(ctx, in.ID) - if err == nil && len(conn.MfaOptions) > 0 { + if err != nil { + return fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err) + } + if len(conn.MfaOptions) > 0 { resolved := false for _, opt := range conn.MfaOptions { displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type) diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index 4f12f2d..b34cec2 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -297,6 +297,23 @@ func TestSubmit_MfaOptionResolvesLabelCaseInsensitive(t *testing.T) { assert.Equal(t, "sms", submittedID) } +func TestSubmit_MfaOptionGetErrorSurfaced(t *testing.T) { + fake := &FakeAuthConnectionService{ + GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) { + return nil, errors.New("connection not found") + }, + } + + c := AuthConnectionCmd{svc: fake} + err := c.Submit(context.Background(), AuthConnectionSubmitInput{ + ID: "conn-1", + MfaOptionID: "sms", + Output: "json", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to fetch connection for MFA option resolution") +} + func TestSubmit_MfaOptionRejectsUnknown(t *testing.T) { fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{ {Label: "Get a text", Type: "sms"}, From 3c45ed66d5569ce962093e622963b967788961ef Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Mon, 2 Mar 2026 10:00:37 -0500 Subject: [PATCH 3/3] fix: wrap Get error with CleanedUpSdkError for consistent formatting Matches the pattern used by every other SDK call in this file. Co-Authored-By: Claude Opus 4.6 --- cmd/auth_connections.go | 2 +- cmd/auth_connections_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go index 4f367ec..26851de 100644 --- a/cmd/auth_connections.go +++ b/cmd/auth_connections.go @@ -457,7 +457,7 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn if hasMfaOption { conn, err := c.svc.Get(ctx, in.ID) if err != nil { - return fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err) + return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err)} } if len(conn.MfaOptions) > 0 { resolved := false diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go index b34cec2..ca12c91 100644 --- a/cmd/auth_connections_test.go +++ b/cmd/auth_connections_test.go @@ -311,7 +311,7 @@ func TestSubmit_MfaOptionGetErrorSurfaced(t *testing.T) { Output: "json", }) require.Error(t, err) - assert.Contains(t, err.Error(), "failed to fetch connection for MFA option resolution") + assert.Contains(t, err.Error(), "connection not found") } func TestSubmit_MfaOptionRejectsUnknown(t *testing.T) {