Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cmd/auth_connections.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,37 @@ 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 {
return util.CleanedUpSdkError{Err: 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)
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
}
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.

}
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,
Expand Down
135 changes: 135 additions & 0 deletions cmd/auth_connections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,138 @@ 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_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(), "connection not found")
}

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)")
}