fix(auth): resolve MFA option by label, type, or display string#128
fix(auth): resolve MFA option by label, type, or display string#128
Conversation
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>
There was a problem hiding this comment.
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
Submitnow immediately returnsutil.CleanedUpSdkErrorwhenc.svc.Getfails, preventing unresolved MFA labels from being silently sent to the submit API.
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)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>
There was a problem hiding this comment.
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.
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 | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Bugbot Autofix prepared a fix for the issue found in the latest run.
Or push these changes by commenting: 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 |
masnwilliams
left a comment
There was a problem hiding this comment.
looks good — clean implementation, solid test coverage across all resolution paths and error handling.



Summary
--mfa-option-idnow 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 APIProblem
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 worksTestSubmit_MfaOptionResolvesLabel— passing label resolves to typeTestSubmit_MfaOptionResolvesDisplayString— passing"Label (type)"resolves to typeTestSubmit_MfaOptionResolvesLabelCaseInsensitive— case-insensitive matchingTestSubmit_MfaOptionRejectsUnknown— unknown option returns error with available optionsgo test ./cmd/passes🤖 Generated with Claude Code
Note
Medium Risk
Changes the
auth connections submitMFA selection path by adding an extraGetcall 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 submitnow resolves--mfa-option-idcase-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.