Skip to content

Commit 43fc638

Browse files
rgarciaclaude
andauthored
fix(auth): resolve MFA option by label, type, or display string (#128)
## 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 - [x] `TestSubmit_MfaOptionResolvesType` — passing type directly still works - [x] `TestSubmit_MfaOptionResolvesLabel` — passing label resolves to type - [x] `TestSubmit_MfaOptionResolvesDisplayString` — passing `"Label (type)"` resolves to type - [x] `TestSubmit_MfaOptionResolvesLabelCaseInsensitive` — case-insensitive matching - [x] `TestSubmit_MfaOptionRejectsUnknown` — unknown option returns error with available options - [x] Full `go test ./cmd/` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!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. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3c45ed6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f06b05a commit 43fc638

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

cmd/auth_connections.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,37 @@ func (c AuthConnectionCmd) Submit(ctx context.Context, in AuthConnectionSubmitIn
450450
return fmt.Errorf("must provide at least one of: --field, --mfa-option-id, or --sso-button-selector")
451451
}
452452

453+
// Resolve MFA option: the user may pass the label (e.g. "Get a text"), the
454+
// type (e.g. "sms"), or the display string ("Get a text (sms)"). The API
455+
// expects the type, so look up the connection's available options and map
456+
// whatever the user provided to the correct type value.
457+
if hasMfaOption {
458+
conn, err := c.svc.Get(ctx, in.ID)
459+
if err != nil {
460+
return util.CleanedUpSdkError{Err: fmt.Errorf("failed to fetch connection for MFA option resolution: %w", err)}
461+
}
462+
if len(conn.MfaOptions) > 0 {
463+
resolved := false
464+
for _, opt := range conn.MfaOptions {
465+
displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type)
466+
if strings.EqualFold(in.MfaOptionID, opt.Type) ||
467+
strings.EqualFold(in.MfaOptionID, opt.Label) ||
468+
strings.EqualFold(in.MfaOptionID, displayName) {
469+
in.MfaOptionID = opt.Type
470+
resolved = true
471+
break
472+
}
473+
}
474+
if !resolved {
475+
available := make([]string, 0, len(conn.MfaOptions))
476+
for _, opt := range conn.MfaOptions {
477+
available = append(available, fmt.Sprintf("%s (%s)", opt.Label, opt.Type))
478+
}
479+
return fmt.Errorf("unknown MFA option %q; available: %s", in.MfaOptionID, strings.Join(available, ", "))
480+
}
481+
}
482+
}
483+
453484
params := kernel.AuthConnectionSubmitParams{
454485
SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
455486
Fields: in.FieldValues,

cmd/auth_connections_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,138 @@ func TestAuthConnectionsList_JSONOutput_PrintsRawResponse(t *testing.T) {
196196
assert.Contains(t, out, "\"profile_name\"")
197197
assert.Contains(t, out, "\"raf-leaseweb\"")
198198
}
199+
200+
func newFakeWithMfaOptions(options []kernel.ManagedAuthMfaOption) *FakeAuthConnectionService {
201+
return &FakeAuthConnectionService{
202+
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
203+
return &kernel.ManagedAuth{
204+
ID: id,
205+
MfaOptions: options,
206+
}, nil
207+
},
208+
SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
209+
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
210+
},
211+
}
212+
}
213+
214+
func TestSubmit_MfaOptionResolvesType(t *testing.T) {
215+
fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
216+
{Label: "Get a text", Type: "sms"},
217+
{Label: "Have us call you", Type: "call"},
218+
})
219+
220+
var submittedID string
221+
fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
222+
submittedID = body.SubmitFieldsRequest.MfaOptionID.Value
223+
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
224+
}
225+
226+
c := AuthConnectionCmd{svc: fake}
227+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
228+
ID: "conn-1",
229+
MfaOptionID: "sms",
230+
Output: "json",
231+
})
232+
require.NoError(t, err)
233+
assert.Equal(t, "sms", submittedID)
234+
}
235+
236+
func TestSubmit_MfaOptionResolvesLabel(t *testing.T) {
237+
fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
238+
{Label: "Get a text", Type: "sms"},
239+
{Label: "Have us call you", Type: "call"},
240+
})
241+
242+
var submittedID string
243+
fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
244+
submittedID = body.SubmitFieldsRequest.MfaOptionID.Value
245+
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
246+
}
247+
248+
c := AuthConnectionCmd{svc: fake}
249+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
250+
ID: "conn-1",
251+
MfaOptionID: "Get a text",
252+
Output: "json",
253+
})
254+
require.NoError(t, err)
255+
assert.Equal(t, "sms", submittedID)
256+
}
257+
258+
func TestSubmit_MfaOptionResolvesDisplayString(t *testing.T) {
259+
fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
260+
{Label: "Get a text", Type: "sms"},
261+
})
262+
263+
var submittedID string
264+
fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
265+
submittedID = body.SubmitFieldsRequest.MfaOptionID.Value
266+
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
267+
}
268+
269+
c := AuthConnectionCmd{svc: fake}
270+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
271+
ID: "conn-1",
272+
MfaOptionID: "Get a text (sms)",
273+
Output: "json",
274+
})
275+
require.NoError(t, err)
276+
assert.Equal(t, "sms", submittedID)
277+
}
278+
279+
func TestSubmit_MfaOptionResolvesLabelCaseInsensitive(t *testing.T) {
280+
fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
281+
{Label: "Get a text", Type: "sms"},
282+
})
283+
284+
var submittedID string
285+
fake.SubmitFunc = func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
286+
submittedID = body.SubmitFieldsRequest.MfaOptionID.Value
287+
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
288+
}
289+
290+
c := AuthConnectionCmd{svc: fake}
291+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
292+
ID: "conn-1",
293+
MfaOptionID: "get a TEXT",
294+
Output: "json",
295+
})
296+
require.NoError(t, err)
297+
assert.Equal(t, "sms", submittedID)
298+
}
299+
300+
func TestSubmit_MfaOptionGetErrorSurfaced(t *testing.T) {
301+
fake := &FakeAuthConnectionService{
302+
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
303+
return nil, errors.New("connection not found")
304+
},
305+
}
306+
307+
c := AuthConnectionCmd{svc: fake}
308+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
309+
ID: "conn-1",
310+
MfaOptionID: "sms",
311+
Output: "json",
312+
})
313+
require.Error(t, err)
314+
assert.Contains(t, err.Error(), "connection not found")
315+
}
316+
317+
func TestSubmit_MfaOptionRejectsUnknown(t *testing.T) {
318+
fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
319+
{Label: "Get a text", Type: "sms"},
320+
{Label: "Have us call you", Type: "call"},
321+
})
322+
323+
c := AuthConnectionCmd{svc: fake}
324+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
325+
ID: "conn-1",
326+
MfaOptionID: "carrier pigeon",
327+
Output: "json",
328+
})
329+
require.Error(t, err)
330+
assert.Contains(t, err.Error(), "unknown MFA option")
331+
assert.Contains(t, err.Error(), "carrier pigeon")
332+
assert.Contains(t, err.Error(), "Get a text (sms)")
333+
}

0 commit comments

Comments
 (0)