Skip to content
Open
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
45 changes: 45 additions & 0 deletions auth/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,50 @@ _ = login
```
</CodeGroup>

## Human verification for CAPTCHAs

Set `hitl: true` to let a person finish a CAPTCHA that the login browser can't solve on its own. Without it, the login fails with `captcha_blocked`. With it, the login pauses at `AWAITING_HUMAN_INTERVENTION` so the person can complete the check in the login browser's live view, then confirm. KERNEL resumes the login from the page they left.

<CodeGroup>
```typescript TypeScript
const auth = await kernel.auth.connections.create({
domain: 'example.com',
profile_name: 'my-profile',
hitl: true,
});
```

```python Python
auth = await kernel.auth.connections.create(
domain="example.com",
profile_name="my-profile",
hitl=True,
)
```

```go Go
auth, err := client.Auth.Connections.New(ctx, kernel.AuthConnectionNewParams{
ManagedAuthCreateRequest: kernel.ManagedAuthCreateRequestParam{
Domain: "example.com",
ProfileName: "my-profile",
Hitl: kernel.Bool(true),
},
})
if err != nil {
panic(err)
}
_ = auth
```
</CodeGroup>

The [Hosted UI](/auth/hosted-ui) and the [React component](/auth/react) embed the live view and show a **Continue** button, so you don't need extra code. In a [programmatic flow](/auth/programmatic#human-verification), you show the live view and submit the confirmation yourself.

How the pause behaves:

- It applies to every login you start with `.login()`, including a [pre-task check](/auth/connection-lifecycle#verify-authentication-before-starting-work) and CAPTCHAs that appear before the login form. Automatic reauthentication after a health check never pauses, because nobody is there to solve the challenge; it still fails with `captcha_blocked`.
- Each pause waits up to 10 minutes, or until the login session expires if that's sooner. If nobody confirms in time, the login fails with `captcha_blocked` and an `error_message` saying a person was asked and didn't finish in time.
- If the site asks for another check after the person continues, the login pauses again. From the second pause on, the confirmation choice's `description` says the site is asking to verify again.

## Record sessions for debugging

Set `record_session: true` to capture a [replay](/browsers/replays) of every browser session tied to the connection — initial logins, background health checks, and automatic re-authentications. The entire browser session is recorded.
Expand Down Expand Up @@ -918,6 +962,7 @@ After creating a connection, you can update its configuration with `auth.connect
| `auto_reauth` | Whether a scheduled health check that confirms a logged-out session may trigger an eligible automatic reauthentication attempt |
| `save_credentials` | Whether to save credentials on successful login |
| `record_session` | Record a [replay](/browsers/replays) of every auth browser session for this connection (logins, health checks, and reauths) |
| `hitl` | Pause interactive logins on an unsolvable CAPTCHA so a person can complete it. See [human verification for CAPTCHAs](#human-verification-for-captchas) |
| `browser.region` | Region for login, health-check, and reauth browsers. Takes effect on the next browser created for the connection |
| `proxy` | Pin login, health-check, and reauth sessions to a proxy. Takes effect on the next health check or reauth |

Expand Down
20 changes: 14 additions & 6 deletions auth/connection-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ Call `.login()` immediately before work that requires an authenticated session.

For a connection with a previous successful login and saved auth check URL, `.login()` runs the verifier first. If the profile is still signed in, the flow reaches `SUCCESS` without submitting anything. If it isn't, KERNEL starts the normal login flow and uses saved credentials when available.

Follow the connection until the flow reaches `SUCCESS`, then create the browser or continue the task. If the flow reaches `AWAITING_INPUT` or `AWAITING_EXTERNAL_ACTION`, stop the pre-task helper and send the user to the `hosted_url` returned by this `.login()` call. Continue that login flow; don't call `.login()` again, because a new call cancels the flow already in progress.
Follow the connection until the flow reaches `SUCCESS`, then create the browser or continue the task. If the flow reaches `AWAITING_INPUT`, `AWAITING_EXTERNAL_ACTION`, or `AWAITING_HUMAN_INTERVENTION`, stop the pre-task helper and send the user to the `hosted_url` returned by this `.login()` call. Continue that login flow; don't call `.login()` again, because a new call cancels the flow already in progress.

If the connection has [`hitl`](/auth/configuration#human-verification-for-captchas) enabled, a CAPTCHA during this check pauses the flow at `AWAITING_HUMAN_INTERVENTION`. In an unattended job, send the `hosted_url` to a person right away: the pause waits up to 10 minutes, and the next run's `.login()` cancels it. If nobody can act on the link in that time, leave `hitl` off for connections used only by unattended jobs.

Use this pattern whether periodic health checks are enabled or disabled:

Expand All @@ -114,7 +116,8 @@ async function ensureAuthenticated(connectionId: string) {

if (
event.flow_step === 'AWAITING_INPUT' ||
event.flow_step === 'AWAITING_EXTERNAL_ACTION'
event.flow_step === 'AWAITING_EXTERNAL_ACTION' ||
event.flow_step === 'AWAITING_HUMAN_INTERVENTION'
) {
throw new Error(`user action required at ${login.hosted_url}`);
}
Expand Down Expand Up @@ -142,7 +145,11 @@ async def ensure_authenticated(connection_id: str):
if event.flow_status == "SUCCESS":
return

if event.flow_step in {"AWAITING_INPUT", "AWAITING_EXTERNAL_ACTION"}:
if event.flow_step in {
"AWAITING_INPUT",
"AWAITING_EXTERNAL_ACTION",
"AWAITING_HUMAN_INTERVENTION",
}:
raise RuntimeError(f"user action required at {login.hosted_url}")

raise RuntimeError("authentication did not complete successfully")
Expand Down Expand Up @@ -174,7 +181,8 @@ for events.Next() {
break
}

if event.FlowStep == "AWAITING_INPUT" || event.FlowStep == "AWAITING_EXTERNAL_ACTION" {
switch event.FlowStep {
case "AWAITING_INPUT", "AWAITING_EXTERNAL_ACTION", "AWAITING_HUMAN_INTERVENTION":
panic("user action required at " + login.HostedURL)
}
}
Expand Down Expand Up @@ -220,7 +228,7 @@ Common codes:
| `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 |
| `captcha_blocked` | A CAPTCHA was presented and couldn't be solved, or a person was asked to solve it and didn't finish in time. | No |

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

Expand All @@ -232,7 +240,7 @@ See the [API reference](https://kernel.sh/docs/api-reference/managed-auth/start-
- **`account_choice_required` / `customer_input_required` / `external_action_required`** — Start an interactive login and complete the choice, field, or external approval. Kernel does not guess an identity or trigger notification-producing steps during unattended reauth.
- **`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).
- **`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). For `captcha_blocked` on interactive logins, enable [`hitl`](/auth/configuration#human-verification-for-captchas) so a person can complete the CAPTCHA in the live view instead of the login failing.
- **`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.

Expand Down
1 change: 1 addition & 0 deletions auth/hosted-ui.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ The user will:
1. See the login page for the target website
2. Enter their credentials
3. Complete 2FA or another verification step if needed
4. If the connection has [`hitl`](/auth/configuration#human-verification-for-captchas) enabled and the site shows a CAPTCHA the login browser can't solve, complete it in the login browser's live view shown on the page, then click **Continue**. This runs on KERNEL's page, so your CSP isn't involved

<Info>
KERNEL can attempt automatic reauthentication for eligible flows using stored credentials, including totp codes generated from an available secret. completing this interactive login doesn't guarantee unattended reauthentication. supplying an email or sms code doesn't give KERNEL access to future codes. when the connection needs user input again, start a new login and direct the user to its `hosted_url`. see [connection recovery](/auth/connection-lifecycle#flows-that-need-input-a-choice-or-approval).
Expand Down
133 changes: 125 additions & 8 deletions auth/programmatic.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use the programmatic flow when:
Create a managed auth connection, then call `.login()`.
</Step>
<Step title="Stream the session state">
Follow the connection's sse stream. When `flow_step` becomes `AWAITING_INPUT`, render `fields` and `choices` from the event.
Follow the connection's sse stream. When `flow_step` becomes `AWAITING_INPUT`, render `fields` and `choices` from the event. When it becomes `AWAITING_HUMAN_INTERVENTION`, show the login browser's [live view](#human-verification).
</Step>
<Step title="Submit one interaction">
Send the event's `interaction_id` with either `field_values` or `selected_choice_id`. Keep listening because the next page may produce another interaction.
Expand Down Expand Up @@ -94,17 +94,45 @@ a successful interactive login can save durable credentials for eligible automat

### 3. Stream and submit

Listen for `AWAITING_INPUT`. Submit fields or a selected choice, then keep listening for the next interaction.
Listen for `AWAITING_INPUT` and `AWAITING_HUMAN_INTERVENTION`. Submit fields, a selected choice, or the human verification choice, then keep listening for the next interaction.

```typescript TypeScript
const events = await kernel.auth.connections.follow(auth.id);

for await (const event of events) {
if (
event.event !== 'managed_auth_state' ||
event.flow_step !== 'AWAITING_INPUT' ||
!event.interaction_id
) {
if (event.event !== 'managed_auth_state') {
continue;
}

if (event.flow_step !== 'AWAITING_HUMAN_INTERVENTION') {
closeLiveView();
}

if (!event.interaction_id) {
continue;
}

if (event.flow_step === 'AWAITING_HUMAN_INTERVENTION') {
const done = event.choices?.find(choice => choice.type === 'human_intervention');
if (!done) {
continue;
}

const interactionId = event.interaction_id;
const liveViewUrl =
event.live_view_url ?? (await kernel.auth.connections.retrieve(auth.id)).live_view_url;

// Don't await the person here, so the loop still sees a timeout.
showLiveView(liveViewUrl, done.description, () =>
kernel.auth.connections.submit(auth.id, {
interaction_id: interactionId,
selected_choice_id: done.id,
}),
);
continue;
}

if (event.flow_step !== 'AWAITING_INPUT') {
continue;
}

Expand All @@ -130,7 +158,7 @@ for await (const event of events) {
}
```

`promptUser` and `promptUserToChoose` represent your application's ui. The submission examples below show the same requests in each sdk.
`promptUser`, `promptUserToChoose`, `showLiveView`, and `closeLiveView` represent your application's ui. The submission examples below show the same requests in each sdk.

<Tip>
every programmatic login session also has a `hosted_url`. redirect the user there if you want the hosted ui to finish an unexpected state.
Expand Down Expand Up @@ -237,6 +265,7 @@ All selectable auth routes use the same shape. `choice.type` identifies the cate
- `auth_method`
- `identifier_method`
- `account`
- `human_intervention`, used only at [human verification](#human-verification) pauses
- `other`

Render the visible `label`, optional `description`, and optional `masked_destination`. Submit the exact `choice.id` returned by the event:
Expand Down Expand Up @@ -312,13 +341,101 @@ if (
}
```

## Human verification

When the connection has [`hitl`](/auth/configuration#human-verification-for-captchas) enabled and the login browser hits a CAPTCHA it can't solve, `flow_step` becomes `AWAITING_HUMAN_INTERVENTION`. The event carries one choice with `type: 'human_intervention'` and id `human_intervention_done`.

Show the person the login browser's live view, in a new tab or embedded in an iframe, with a button to click when they've finished the check. When they click it, submit the choice with the event's `interaction_id`. KERNEL resumes the login from the page they left.

Keep reading the stream while the live view is open, as the [stream and submit](#3-stream-and-submit) loop does. If nobody submits in time, the next event is `FAILED` with `error_code: 'captcha_blocked'`, and your ui must close the live view.

The event's `live_view_url` can be missing. When it is, read `live_view_url` from the connection:

<CodeGroup>
```typescript TypeScript
const done = state.choices?.find(choice => choice.type === 'human_intervention');
const liveViewUrl =
state.live_view_url ?? (await kernel.auth.connections.retrieve(auth.id)).live_view_url;

// After the person clicks your button:
if (done && state.interaction_id) {
await kernel.auth.connections.submit(auth.id, {
interaction_id: state.interaction_id,
selected_choice_id: done.id,
});
}
```

```python Python
done = next(
(choice for choice in (state.choices or []) if choice.type == "human_intervention"),
None,
)
live_view_url = state.live_view_url or (
await kernel.auth.connections.retrieve(auth.id)
).live_view_url

# After the person clicks your button:
if done and state.interaction_id:
await kernel.auth.connections.submit(
auth.id,
interaction_id=state.interaction_id,
selected_choice_id=done.id,
)
```

```go Go
liveViewURL := state.LiveViewURL
if liveViewURL == "" {
connection, err := client.Auth.Connections.Get(ctx, auth.ID)
if err != nil {
panic(err)
}
liveViewURL = connection.LiveViewURL
}

// After the person clicks your button:
for _, choice := range state.Choices {
if choice.Type != "human_intervention" {
continue
}
_, err := client.Auth.Connections.Submit(ctx, auth.ID, kernel.AuthConnectionSubmitParams{
SubmitFieldsRequest: kernel.SubmitFieldsRequestParam{
InteractionID: kernel.String(state.InteractionID),
SelectedChoiceID: kernel.String(choice.ID),
},
})
if err != nil {
panic(err)
}
}
```
</CodeGroup>

- At this step the submit endpoint accepts only the `human_intervention` choice. Field values and other choices return a 400.
- KERNEL can't tell whether the person solved the check before submitting. If the site still shows a challenge, or asks again, the login pauses again with a new `interaction_id`. From the second pause on, the choice's `description` says the site is asking to verify again. Show it so the person knows why they're back. On the first pause, `description` is empty.
- Each pause waits up to 10 minutes, or until the login session expires if that's sooner.
- The live view gives full control of the login browser. Show it only to the person signing in, and don't log the url.

If your page sets a Content-Security-Policy, allow live view frames from both KERNEL domains. Don't copy the hostname from a single `live_view_url`, because it can change between sessions:

```text
frame-src https://*.onkernel.com
https://*.kernel.sh
https://*.onkernel.com:8443
https://*.kernel.sh:8443;
```

See [content security policy](/info/network-access#content-security-policy) for the full requirements.

## Step reference

| Step | Description |
|---|---|
| `DISCOVERING` | Finding and inspecting the login surface. |
| `AWAITING_INPUT` | Waiting for canonical fields or choices. |
| `AWAITING_EXTERNAL_ACTION` | Waiting for an out-of-browser action; canonical fallbacks may also be present. |
| `AWAITING_HUMAN_INTERVENTION` | Waiting for a person to complete a CAPTCHA in the live view and submit the `human_intervention` choice. Only on connections with `hitl` enabled. |
| `SUBMITTING` | Processing the submitted interaction. |
| `COMPLETED` | The flow has finished. |

Expand Down
22 changes: 21 additions & 1 deletion auth/react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "React Component"
description: "Embed the Kernel managed auth login flow in your own app with a drop-in React component"
---

[`@onkernel/managed-auth-react`](https://www.npmjs.com/package/@onkernel/managed-auth-react) is a React component library that renders the entire managed auth login flow inside your own app. It calls the same APIs as the [Hosted UI](/auth/hosted-ui), but renders on your origin so it inherits your styling, domain, and CSP.
[`@onkernel/managed-auth-react`](https://www.npmjs.com/package/@onkernel/managed-auth-react) is a React component library that renders the entire managed auth login flow inside your own app. It calls the same APIs as the [Hosted UI](/auth/hosted-ui), but renders on your origin so it inherits your styling, domain, and CSP. The one exception is [human verification](#human-verification), which embeds the login browser's live view from a KERNEL domain.

Use the React component when:
- You want the [Hosted UI](/auth/hosted-ui) experience but rendered inside your own app
Expand Down Expand Up @@ -109,6 +109,25 @@ rendering the component for the initial login doesn't provide ongoing automatic

call `auth.connections.login()` on the existing connection from your backend, then bring the user back to your login route. pass the new login response's `id` as `sessionId` and its fresh `handoff_code` as `handoffCode`. don't reuse the previous handoff code: it is single-use. wait for successful authentication before resuming work that requires the account. see [connection recovery](/auth/connection-lifecycle#flows-that-need-input-a-choice-or-approval).

## Human verification

When the connection has [`hitl`](/auth/configuration#human-verification-for-captchas) enabled and the login hits a CAPTCHA the browser can't solve, the component embeds the login browser's live view so the person can complete the check, then click **Continue**. If the site asks for another check, the component shows a note explaining why the person is back.

If nobody continues in time, `onError` fires with `code: 'captcha_blocked'` and a message saying a person was asked and didn't finish in time.

If your page sets a Content-Security-Policy, allow live view frames from both KERNEL domains. Otherwise the browser blocks the embedded live view and the person can't complete the check. Don't copy the hostname from a single `live_view_url`, because it can change between sessions:

```text
frame-src https://*.onkernel.com
https://*.kernel.sh
https://*.onkernel.com:8443
https://*.kernel.sh:8443;
```

See [content security policy](/info/network-access#content-security-policy) for the full requirements.

This requires `@onkernel/managed-auth-react` 0.6.0 or later.

## Backend connectivity

By default the component talks directly to `https://api.onkernel.com`. That works out of the box; nothing else to configure.
Expand Down Expand Up @@ -262,6 +281,7 @@ import {
StepExpired,
LoadingState,
ExternalActionWaiting,
HumanInterventionStep,
UnifiedAuthForm,
AppearanceProvider,
LocalizationProvider,
Expand Down
Loading