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
36 changes: 24 additions & 12 deletions auth/connection-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ After the initial login, every connection moves through this loop:

<Steps>
<Step title="Health check">
On a configurable cadence, Kernel spins up a browser with the profile and verifies the session is still logged in. If it is, nothing else happens until the next check.
On a configurable cadence, Kernel spins up a browser with the profile and verifies the session. A check reaches one of three conclusions: still logged in, definitely logged out, or **inconclusive** — the page didn't prove either way.

A check needs concrete evidence to conclude. If the site returns a challenge page, times out, partially loads, or shows something ambiguous, the result is inconclusive and the connection is left exactly as it was. Nothing happens until the next check.
</Step>
<Step title="Auto-reauth (if eligible)">
If the check finds the session expired and the connection's `can_reauth` is `true`, Kernel runs the saved login flow with the stored credentials in the background. A successful login resets the loop.
If the check finds the session **definitely** expired and the connection's `can_reauth` is `true`, Kernel runs the saved login flow with the stored credentials in the background. A successful login resets the loop.

An inconclusive check never triggers reauth. Kernel would rather check again on the next cycle than log in again unnecessarily — a spurious login can trip risk checks on the site, prompt a device-verification email, or invalidate a working session.
</Step>
<Step title="NEEDS_AUTH (if not eligible, or auto-reauth fails)">
If auto-reauth isn't possible — credentials aren't linked, the saved flow requires human input, or the login keeps failing — the connection's `status` flips to `NEEDS_AUTH` and a new login session is required.
Expand Down Expand Up @@ -71,9 +75,9 @@ If you're seeing the connection flip to `NEEDS_AUTH` frequently and want shorter
Check the `can_reauth` boolean on a connection. It's `true` only when **both** of these hold:

1. **A credential is linked** — stored in Kernel or sourced via [1Password](/integrations/1password).
2. **No external action is required** — the saved login flow doesn't need a human (no SMS/email OTP, no push notification, no manual MFA selection).
2. **No known human-only requirement is recorded** — for example, an sms/email code, push approval, or other external action.

If either fails, the connection will move to `NEEDS_AUTH` on the next expired session and wait for a fresh login.
If either fails, the connection will move to `NEEDS_AUTH` on the next expired session and wait for a fresh login. A site can still present a new or conditional challenge during reauth; if Kernel can't resolve it safely, reauth stops and the connection moves to `NEEDS_AUTH`.

### External actions that block auto-reauth

Expand Down Expand Up @@ -127,24 +131,32 @@ if state.Status == kernel.ManagedAuthStatusNeedsAuth {

## When a login fails

If a login attempt fails — whether triggered by a health check, an auto-reauth, or a manual `.login()` — Kernel retries with exponential backoff. After repeated failures the flow is marked failed and the connection surfaces an error code on `flow_status`.
If a login attempt fails — whether triggered by a health check, an auto-reauth, or a manual `.login()` — the flow is marked `FAILED` and the event includes `error_code` and `error_message`.

How much Kernel retries depends on the failure. A transient site problem (a 5xx page, a maintenance screen) is retried once against the login page before giving up. A conclusive rejection by the site — wrong credentials, a locked account, an unsupported method — is **not** retried, because retrying would burn attempts against a lockout or fail identically.

Common codes:

| Code | Meaning |
|------|---------|
| `credentials_invalid` | The stored or submitted credentials were rejected by the site. |
| `bot_detected` | The login page blocked the session as automated. |
| `captcha_blocked` | A CAPTCHA was presented and couldn't be solved. |
| `unsupported_auth_method` | The site required a method Kernel doesn't currently support (e.g. passkeys). |
| Code | Meaning | Retried |
|------|---------|---------|
| `credentials_invalid` | The stored or submitted credentials were rejected by the site. | No |
| `account_locked` | The site locked or suspended the account. | No |
| `unsupported_auth_method` | The site required a method Kernel doesn't currently support (e.g. passkeys). | No |
| `rate_limited` | The site rate-limited the login attempt. | No; retry later |
| `website_error` | The site returned an error, maintenance, or unavailable page. | Once |
| `bot_detected` | The login page blocked the session as automated. | No |
| `captcha_blocked` | A CAPTCHA was presented and couldn't be solved. | No |

See the [API reference](https://kernel.sh/docs/api-reference/managed-auth/start-login-flow) for the full list.

### Recovering

- **`credentials_invalid`** — Update the linked [credential](/auth/credentials) and call `.login()` to re-run the flow.
- **`credentials_invalid`** — Update the linked [credential](/auth/credentials) and call `.login()` to re-run the flow. When the site identifies which field it rejected during an interactive login, Kernel asks for a corrected value in place — see [replacing a rejected credential](/auth/programmatic#replacing-a-rejected-credential).
- **`account_locked`** — Unlock the account with the site directly. Calling `.login()` again before that will not help and may extend the lockout.
- **`rate_limited`** — Wait for the site's retry window before starting another login.
- **`bot_detected` / `captcha_blocked`** — Pin the connection to a cleaner [proxy](/auth/configuration#custom-proxy) (ISP or custom). For aggressive sites, also enable stealth and review the [bot detection guide](/browsers/bot-detection/overview).
- **`unsupported_auth_method`** — Switch the account to a supported sign-in method (e.g. password + TOTP instead of a passkey) and re-link the credential.
- **`website_error`** — Usually the site, not the connection. Retry later; if it persists, confirm the connection's `login_url` still points at a working login page.

## Debugging a flaky connection

Expand Down
37 changes: 28 additions & 9 deletions auth/credentials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,17 @@ const login = await kernel.auth.connections.login(auth.id);
// Stream state changes and submit the missing password
const authEvents = await kernel.auth.connections.follow(auth.id);
for await (const event of authEvents) {
const passwordField = event.fields?.find(field => field.ref === 'password');
if (
event.event === 'managed_auth_state' &&
event.flow_step === 'AWAITING_INPUT' &&
event.discovered_fields?.length
event.interaction_id &&
passwordField
) {
// Only password field will be pending (email auto-filled from credential)
// Only password is pending; email is filled from the stored credential.
await kernel.auth.connections.submit(auth.id, {
fields: { password: 'user-provided-password' }
interaction_id: event.interaction_id,
field_values: { [passwordField.id]: 'user-provided-password' },
});
}
}
Expand All @@ -347,15 +350,21 @@ login = await kernel.auth.connections.login(auth.id)
# Stream state changes and submit the missing password
auth_events = await kernel.auth.connections.follow(auth.id)
async for event in auth_events:
password_field = next(
(field for field in (event.fields or []) if field.ref == "password"),
None,
)
if (
event.event == "managed_auth_state"
and event.flow_step == "AWAITING_INPUT"
and event.discovered_fields
and event.interaction_id
and password_field
):
# Only password field will be pending (email auto-filled from credential)
# Only password is pending; email is filled from the stored credential.
await kernel.auth.connections.submit(
auth.id,
fields={"password": "user-provided-password"},
interaction_id=event.interaction_id,
field_values={password_field.id: "user-provided-password"},
)
# TOTP auto-submitted from credential → SUCCESS
```
Expand Down Expand Up @@ -398,16 +407,26 @@ _ = login
authEvents := client.Auth.Connections.FollowStreaming(ctx, auth.ID)
for authEvents.Next() {
event := authEvents.Current()
if event.Event == "managed_auth_state" && event.FlowStep == "AWAITING_INPUT" && len(event.DiscoveredFields) > 0 {
// Only password field will be pending (email auto-filled from credential)
if event.Event != "managed_auth_state" || event.FlowStep != "AWAITING_INPUT" || event.InteractionID == "" {
continue
}
for _, field := range event.Fields {
if field.Ref != "password" {
continue
}
// Only password is pending; email is filled from the stored credential.
_, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{
SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
Fields: map[string]string{"password": "user-provided-password"},
InteractionID: kernel.String(event.InteractionID),
FieldValues: map[string]string{
field.ID: "user-provided-password",
},
},
})
if err != nil {
panic(err)
}
break
}
}
if err := authEvents.Err(); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions auth/faq.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ title: FAQ

When you link credentials to a connection, Kernel runs periodic health checks, detects logged-out sessions, and re-authenticates in the background so the profile stays logged in. See [Connection Lifecycle](/auth/connection-lifecycle) for the full lifecycle, cadence options, and `can_reauth` rules.

## What are sign-in options?
## What are auth choices?

Sign-in options are non-MFA choices that some sites present during login, such as account pickers ("Which account do you want to use?") or organization selectors. Unlike MFA options which are security challenges, sign-in options are informational choices that route the login flow. They appear in the session state as `sign_in_options` and are submitted via `sign_in_option_id`. See the [Programmatic flow guide](/auth/programmatic#sign-in-options-accountorg-pickers) for details and SDK examples.
Auth choices are visible routes a site presents during login, including mfa methods, sso providers, account pickers, and organization selectors. They appear in the canonical `choices` array. Submit the exact returned id with `interaction_id` and `selected_choice_id`. See the [programmatic flow guide](/auth/programmatic#choices) for examples.

## Which authentication methods are supported?

Expand All @@ -20,7 +20,7 @@ Passkey-based authentication (e.g., Google accounts with passkeys enabled) is no

## What happens if login fails?

Kernel retries with exponential backoff, then surfaces an error code (`credentials_invalid`, `bot_detected`, `captcha_blocked`, etc.). See [Connection Lifecycle](/auth/connection-lifecycle#when-a-login-fails) for the full list and recovery steps.
Kernel surfaces an error code (`credentials_invalid`, `account_locked`, `bot_detected`, `captcha_blocked`, etc.). Transient site failures are retried; a conclusive rejection by the site isn't, so Kernel doesn't burn attempts against a locked account or resubmit credentials the site already refused. See [Connection Lifecycle](/auth/connection-lifecycle#when-a-login-fails) for the full list and recovery steps.

## Can I use Managed Auth with any website?

Expand Down
Loading
Loading