Skip to content

Commit e6ba11a

Browse files
rgarciaclaude
andcommitted
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 <noreply@anthropic.com>
1 parent f06b05a commit e6ba11a

2 files changed

Lines changed: 146 additions & 0 deletions

File tree

cmd/auth_connections.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,34 @@ 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 && len(conn.MfaOptions) > 0 {
460+
resolved := false
461+
for _, opt := range conn.MfaOptions {
462+
displayName := fmt.Sprintf("%s (%s)", opt.Label, opt.Type)
463+
if strings.EqualFold(in.MfaOptionID, opt.Type) ||
464+
strings.EqualFold(in.MfaOptionID, opt.Label) ||
465+
strings.EqualFold(in.MfaOptionID, displayName) {
466+
in.MfaOptionID = opt.Type
467+
resolved = true
468+
break
469+
}
470+
}
471+
if !resolved {
472+
available := make([]string, 0, len(conn.MfaOptions))
473+
for _, opt := range conn.MfaOptions {
474+
available = append(available, fmt.Sprintf("%s (%s)", opt.Label, opt.Type))
475+
}
476+
return fmt.Errorf("unknown MFA option %q; available: %s", in.MfaOptionID, strings.Join(available, ", "))
477+
}
478+
}
479+
}
480+
453481
params := kernel.AuthConnectionSubmitParams{
454482
SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
455483
Fields: in.FieldValues,

cmd/auth_connections_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,121 @@ 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_MfaOptionRejectsUnknown(t *testing.T) {
301+
fake := newFakeWithMfaOptions([]kernel.ManagedAuthMfaOption{
302+
{Label: "Get a text", Type: "sms"},
303+
{Label: "Have us call you", Type: "call"},
304+
})
305+
306+
c := AuthConnectionCmd{svc: fake}
307+
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
308+
ID: "conn-1",
309+
MfaOptionID: "carrier pigeon",
310+
Output: "json",
311+
})
312+
require.Error(t, err)
313+
assert.Contains(t, err.Error(), "unknown MFA option")
314+
assert.Contains(t, err.Error(), "carrier pigeon")
315+
assert.Contains(t, err.Error(), "Get a text (sms)")
316+
}

0 commit comments

Comments
 (0)