Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -619,6 +619,7 @@ Managed auth connections (`kernel auth connections`). The commands below are new
- `kernel auth connections submit <id>` - New flags:
- `--field-value <id=value>` - Canonical field-id=value pair from the connection's `fields` list (repeatable); preferred over the legacy `--field`
- `--choice-id <id>` - Canonical choice ID from the connection's `choices` list
- `--interaction-id <id>` - Canonical interaction the submitted values answer. Only valid with `--field-value` or `--choice-id`; omit it and the CLI reads the connection's current interaction ID for you. Pass it to pin the submission, so the API rejects it if the flow has already moved on.

`kernel auth connections get` and `follow` list those IDs alongside the metadata the API captured for them, so you can tell the options apart before submitting. Fields show their type, ref, and any hint (which names the masked destination a one-time code was sent to); choices show their type, semantic MFA method (`sms`, `totp`, `push`, …), and masked destination.

Expand Down
160 changes: 111 additions & 49 deletions cmd/auth_connections.go

Large diffs are not rendered by default.

132 changes: 122 additions & 10 deletions cmd/auth_connections_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -147,13 +147,17 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) {
Status: kernel.ManagedAuthStatusNeedsAuth,
FlowStatus: kernel.ManagedAuthFlowStatusInProgress,
FlowStep: kernel.ManagedAuthFlowStepAwaitingInput,
// Canonical fields and choices always arrive with the interaction
// they belong to, which `submit` needs.
InteractionID: "mai_abc123xyz",
Fields: []kernel.ManagedAuthField{
{
ID: "otp",
Label: "One-time code",
Type: "code",
Ref: "totp_code",
Hint: "Enter the code sent to +1 ••• ••• 1234",
Reason: "rejected",
Required: true,
},
},
Expand DownExpand Up@@ -181,8 +185,11 @@ func TestAuthConnectionsGet_PrintsCanonicalInputMetadata(t *testing.T) {
require.NoError(t, c.Get(context.Background(), AuthConnectionGetInput{ID: "e0x3vbw4z66kpwny3k5k46tj"}))

out := outBuf.String()
assert.Contains(t, out, `mai_abc123xyz`)
assert.Contains(t, out, `otp (One-time code)`)
assert.Contains(t, out, `code, ref=totp_code, required`)
// The reason tells the user why the field is being asked for: "rejected"
// means a stored credential was refused, so a new value has to replace it.
assert.Contains(t, out, `code, ref=totp_code, required, reason=rejected`)
assert.Contains(t, out, `hint="Enter the code sent to +1 ••• ••• 1234"`)
assert.Contains(t, out, `mfa_sms (Text message)`)
assert.Contains(t, out, `mfa_method, sms, to=+1 ••• ••• 1234`)
Expand DownExpand Up@@ -820,16 +827,24 @@ func TestLogin_TelemetryOverride(t *testing.T) {
assert.True(t, captured.Browser.Telemetry.Browser.Screenshot.Enabled.Value)
}

func TestSubmit_CanonicalChoiceID(t *testing.T) {
capturePtermOutput(t)
var captured kernel.AuthConnectionSubmitParams
fake := &FakeAuthConnectionService{
// canonicalSubmitFake serves the current interaction ID from `get` and captures
// what `submit` sends, which is what every canonical submission needs.
func canonicalSubmitFake(interactionID string, captured *kernel.AuthConnectionSubmitParams) *FakeAuthConnectionService {
return &FakeAuthConnectionService{
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
return &kernel.ManagedAuth{ID: id, InteractionID: interactionID}, nil
},
SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
captured = body
*captured = body
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
},
}
c := AuthConnectionCmd{svc: fake}
}

func TestSubmit_CanonicalChoiceID(t *testing.T) {
capturePtermOutput(t)
var captured kernel.AuthConnectionSubmitParams
c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)}
require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
SelectedChoiceID: "choice_sms",
Expand All@@ -841,6 +856,53 @@ func TestSubmit_CanonicalChoiceID(t *testing.T) {
}

func TestSubmit_CanonicalFieldValues(t *testing.T) {
capturePtermOutput(t)
var captured kernel.AuthConnectionSubmitParams
c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)}
require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
CanonicalFieldValues: map[string]string{"field_email": "me@example.com"},
}))
assert.Equal(t, map[string]string{"field_email": "me@example.com"}, captured.SubmitFieldsRequest.FieldValues)
assert.Nil(t, captured.SubmitFieldsRequest.Fields)
}

func TestSubmit_CanonicalResolvesCurrentInteractionID(t *testing.T) {
capturePtermOutput(t)
var captured kernel.AuthConnectionSubmitParams
c := AuthConnectionCmd{svc: canonicalSubmitFake("mai_current", &captured)}
require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
CanonicalFieldValues: map[string]string{"field_email": "me@example.com"},
}))
require.True(t, captured.SubmitFieldsRequest.InteractionID.Valid())
assert.Equal(t, "mai_current", captured.SubmitFieldsRequest.InteractionID.Value)
}

func TestSubmit_ExplicitInteractionIDIsNotOverwritten(t *testing.T) {
capturePtermOutput(t)
var captured kernel.AuthConnectionSubmitParams
fake := canonicalSubmitFake("mai_current", &captured)
getCalls := 0
inner := fake.GetFunc
fake.GetFunc = func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
getCalls++
return inner(ctx, id, opts...)
}
c := AuthConnectionCmd{svc: fake}
require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
SelectedChoiceID: "choice_sms",
// Pinning an older interaction is how a caller detects that the flow moved
// on, so the CLI must forward it untouched.
InteractionID: "mai_pinned",
}))
assert.Equal(t, 0, getCalls)
require.True(t, captured.SubmitFieldsRequest.InteractionID.Valid())
assert.Equal(t, "mai_pinned", captured.SubmitFieldsRequest.InteractionID.Value)
}

func TestSubmit_LegacyModeOmitsInteractionID(t *testing.T) {
capturePtermOutput(t)
var captured kernel.AuthConnectionSubmitParams
fake := &FakeAuthConnectionService{
Expand All@@ -851,11 +913,61 @@ func TestSubmit_CanonicalFieldValues(t *testing.T) {
}
c := AuthConnectionCmd{svc: fake}
require.NoError(t, c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
FieldValues: map[string]string{"username": "me"},
}))
// The API rejects an interaction ID paired with a legacy submit mode.
assert.False(t, captured.SubmitFieldsRequest.InteractionID.Valid())
}

func TestSubmit_InteractionIDRequiresCanonicalMode(t *testing.T) {
capturePtermOutput(t)
c := AuthConnectionCmd{svc: &FakeAuthConnectionService{}}
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
FieldValues: map[string]string{"username": "me"},
InteractionID: "mai_current",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "the --interaction-id flag is only valid with --field-value or --choice-id")
}

func TestSubmit_CanonicalWithoutPendingInteractionErrors(t *testing.T) {
capturePtermOutput(t)
submitted := false
fake := &FakeAuthConnectionService{
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
return &kernel.ManagedAuth{ID: id}, nil
},
SubmitFunc: func(ctx context.Context, id string, body kernel.AuthConnectionSubmitParams, opts ...option.RequestOption) (*kernel.SubmitFieldsResponse, error) {
submitted = true
return &kernel.SubmitFieldsResponse{Accepted: true}, nil
},
}
c := AuthConnectionCmd{svc: fake}
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
SelectedChoiceID: "choice_sms",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "no canonical interaction awaiting input")
assert.False(t, submitted)
}

func TestSubmit_CanonicalGetErrorSurfaced(t *testing.T) {
capturePtermOutput(t)
fake := &FakeAuthConnectionService{
GetFunc: func(ctx context.Context, id string, opts ...option.RequestOption) (*kernel.ManagedAuth, error) {
return nil, errors.New("boom")
},
}
c := AuthConnectionCmd{svc: fake}
err := c.Submit(context.Background(), AuthConnectionSubmitInput{
ID: "auth_1",
CanonicalFieldValues: map[string]string{"field_email": "me@example.com"},
}))
assert.Equal(t, map[string]string{"field_email": "me@example.com"}, captured.SubmitFieldsRequest.FieldValues)
assert.Nil(t, captured.SubmitFieldsRequest.Fields)
})
require.Error(t, err)
assert.Contains(t, err.Error(), "interaction ID resolution")
}

func TestSubmit_CanonicalAndLegacyAreMutuallyExclusive(t *testing.T) {
Expand Down
Loading
Loading