diff --git a/auth/configuration.mdx b/auth/configuration.mdx index dd5aaf8b..25151fa6 100644 --- a/auth/configuration.mdx +++ b/auth/configuration.mdx @@ -799,6 +799,50 @@ _ = login ``` +## 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. + + +```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 +``` + + +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. @@ -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 | diff --git a/auth/connection-lifecycle.mdx b/auth/connection-lifecycle.mdx index 3b194279..77f24b67 100644 --- a/auth/connection-lifecycle.mdx +++ b/auth/connection-lifecycle.mdx @@ -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: @@ -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}`); } @@ -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") @@ -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) } } @@ -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. @@ -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. diff --git a/auth/hosted-ui.mdx b/auth/hosted-ui.mdx index bcb0e6f8..1415dde8 100644 --- a/auth/hosted-ui.mdx +++ b/auth/hosted-ui.mdx @@ -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 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). diff --git a/auth/programmatic.mdx b/auth/programmatic.mdx index 5cbd6f46..137776ef 100644 --- a/auth/programmatic.mdx +++ b/auth/programmatic.mdx @@ -18,7 +18,7 @@ use the programmatic flow when: Create a managed auth connection, then call `.login()`. - 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). Send the event's `interaction_id` with either `field_values` or `selected_choice_id`. Keep listening because the next page may produce another interaction. @@ -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; } @@ -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. every programmatic login session also has a `hosted_url`. redirect the user there if you want the hosted ui to finish an unexpected state. @@ -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: @@ -312,6 +341,93 @@ 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: + + +```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) + } +} +``` + + +- 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 | @@ -319,6 +435,7 @@ if ( | `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. | diff --git a/auth/react.mdx b/auth/react.mdx index beecc847..b315de35 100644 --- a/auth/react.mdx +++ b/auth/react.mdx @@ -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 @@ -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. @@ -262,6 +281,7 @@ import { StepExpired, LoadingState, ExternalActionWaiting, + HumanInterventionStep, UnifiedAuthForm, AppearanceProvider, LocalizationProvider,