Skip to content

fix(auth): resolve MFA option by label, type, or display string#128

Merged
rgarcia merged 3 commits intomainfrom
fix/mfa-option-id-label-resolution
Mar 2, 2026
Merged

fix(auth): resolve MFA option by label, type, or display string#128
rgarcia merged 3 commits intomainfrom
fix/mfa-option-id-label-resolution

Conversation

@rgarcia
Copy link
Contributor

@rgarcia rgarcia commented Mar 2, 2026

Summary

  • --mfa-option-id now accepts the MFA option label (e.g. "Get a text"), type (e.g. "sms"), or the display string (e.g. "Get a text (sms)") — all resolve to the correct type before hitting the API
  • Unknown MFA options now produce a clear error listing available choices instead of silently looping

Problem

When PayPal presented MFA options like "Get a text (sms)", users naturally passed --mfa-option-id "Get a text" (the label). The CLI sent this directly to the API, which expects the type ("sms"). The backend didn't recognize the label, causing the flow to silently loop back to the MFA selection screen with no error.

Fix

Before submitting, the CLI now fetches the connection's available MFA options and resolves the user's input (case-insensitive) against label, type, or combined display string. The resolved type is always sent to the API.

Test plan

  • TestSubmit_MfaOptionResolvesType — passing type directly still works
  • TestSubmit_MfaOptionResolvesLabel — passing label resolves to type
  • TestSubmit_MfaOptionResolvesDisplayString — passing "Label (type)" resolves to type
  • TestSubmit_MfaOptionResolvesLabelCaseInsensitive — case-insensitive matching
  • TestSubmit_MfaOptionRejectsUnknown — unknown option returns error with available options
  • Full go test ./cmd/ passes

🤖 Generated with Claude Code


Note

Medium Risk
Changes the auth connections submit MFA selection path by adding an extra Get call and transforming user input before sending it to the API, which could affect login flows if option resolution mismatches or the prefetch fails.

Overview
auth connections submit now resolves --mfa-option-id case-insensitively against the connection’s available MFA options, accepting a type, label, or "Label (type)" display string and always submitting the resolved option type to the API.

If the provided MFA option doesn’t match any available option, submission now fails fast with a clear error listing the valid choices, and new unit tests cover the supported inputs plus fetch/error handling.

Written by Cursor Bugbot for commit 3c45ed6. This will update automatically on new commits. Configure here.

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 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Silent fallthrough on Get error skips MFA resolution
    • Submit now immediately returns util.CleanedUpSdkError when c.svc.Get fails, preventing unresolved MFA labels from being silently sent to the submit API.

Create PR

Or push these changes by commenting:

@cursor push c74bb9f32e
Preview (c74bb9f32e)
diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go
--- a/cmd/auth_connections.go
+++ b/cmd/auth_connections.go
@@ -456,7 +456,10 @@
 	// 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 util.CleanedUpSdkError{Err: err}
+		}
+		if len(conn.MfaOptions) > 0 {
 			resolved := false
 			for _, opt := range conn.MfaOptions {
 				displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type)

@rgarcia rgarcia requested a review from masnwilliams March 2, 2026 14:32
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 <noreply@anthropic.com>
Matches the pattern used by every other SDK call in this file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Label match can shadow exact type match on later option
    • I changed MFA option resolution to use a three-pass lookup (type, then label, then display string) and added a regression test to ensure type matches are prioritized regardless of option order.

Create PR

Or push these changes by commenting:

@cursor push b3607bf5ed
Preview (b3607bf5ed)
diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go
--- a/cmd/auth_connections.go
+++ b/cmd/auth_connections.go
@@ -460,24 +460,43 @@
 			return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err)}
 		}
 		if len(conn.MfaOptions) > 0 {
-			resolved := false
+			resolvedType := ""
+			inputOption := in.MfaOptionID
+
+			// Match by type first across all options, then label, then display string.
+			// This ensures an exact type match is never shadowed by an earlier label match.
 			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
+				if strings.EqualFold(inputOption, opt.Type) {
+					resolvedType = opt.Type
 					break
 				}
 			}
-			if !resolved {
+			if resolvedType == "" {
+				for _, opt := range conn.MfaOptions {
+					if strings.EqualFold(inputOption, opt.Label) {
+						resolvedType = opt.Type
+						break
+					}
+				}
+			}
+			if resolvedType == "" {
+				for _, opt := range conn.MfaOptions {
+					displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type)
+					if strings.EqualFold(inputOption, displayName) {
+						resolvedType = opt.Type
+						break
+					}
+				}
+			}
+			if resolvedType == "" {
 				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, ", "))
 			}
+
+			in.MfaOptionID = resolvedType
 		}
 	}
 

diff --git a/cmd/auth_connections_test.go b/cmd/auth_connections_test.go
--- a/cmd/auth_connections_test.go
+++ b/cmd/auth_connections_test.go
@@ -233,6 +233,28 @@
 	assert.Equal(t, "sms", submittedID)
 }
 
+func TestSubmit_MfaOptionResolvesTypeBeforeEarlierLabel(t *testing.T) {
+	fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
+		{Label: "call", Type: "phone"},
+		{Label: "Get a text", 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: "call",
+		Output:      "json",
+	})
+	require.NoError(t, err)
+	assert.Equal(t, "call", submittedID)
+}
+
 func TestSubmit_MfaOptionResolvesLabel(t *testing.T) {
 	fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
 		{Label: "Get a text", Type: "sms"},

in.MfaOptionID = opt.Type
resolved = true
break
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Label match can shadow exact type match on later option

Low Severity

The resolution loop checks type, label, and display string for each option before moving to the next. A label match on an earlier MFA option can shadow an exact type match on a later option, resolving to the wrong type. For correct priority, type matches across all options need to be checked before falling back to label matches.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a real concern in practice. MFA types come from a fixed set (sms, call, email, totp, push, password) and it's extremely unlikely a site would have an MFA option whose label exactly matches another option's type string. Adding a multi-pass resolution would add complexity for a scenario that doesn't exist. Leaving as-is.

@cursor
Copy link

cursor bot commented Mar 2, 2026

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: SDK error not wrapped with CleanedUpSdkError
    • The MFA option resolution c.svc.Get error path now wraps the SDK error with util.CleanedUpSdkError while preserving the contextual prefix.

Create PR

Or push these changes by commenting:

@cursor push e4e78f8e6e
Preview (e4e78f8e6e)
diff --git a/cmd/auth_connections.go b/cmd/auth_connections.go
--- a/cmd/auth_connections.go
+++ b/cmd/auth_connections.go
@@ -457,7 +457,7 @@
 	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 fmt.Errorf("failed to fetch connection for MFA option resolution: %w", util.CleanedUpSdkError{Err: err})
 		}
 		if len(conn.MfaOptions) > 0 {
 			resolved := false

Copy link
Contributor

@masnwilliams masnwilliams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good — clean implementation, solid test coverage across all resolution paths and error handling.

@rgarcia rgarcia merged commit 43fc638 into main Mar 2, 2026
3 checks passed
@rgarcia rgarcia deleted the fix/mfa-option-id-label-resolution branch March 2, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants