Skip to content

Latest commit

 

History

History
500 lines (392 loc) · 16.2 KB

File metadata and controls

500 lines (392 loc) · 16.2 KB
title Programmatic Flow
description Build your own credential collection UI with full control

Build your own credential collection UI instead of using the hosted page. Poll for login fields, then submit credentials via the API.

Use the Programmatic flow when:

  • You need a custom credential collection UI that matches your app's design
  • You're building headless/automated authentication
  • You have credentials stored and want to authenticate without user interaction

How It Works

Same as [Hosted UI](/auth/hosted-ui) Poll until `flow_step` becomes `AWAITING_INPUT`, then submit credentials If more fields appear (2FA code), submit again—same loop handles it

Getting started

1. Create a Connection

A Managed Auth Connection attaches an authenticated domain to a profile so you can use the auth connection in future browsers. A single profile can hold multiple auth connections — create one connection for each domain you want to keep authenticated on that profile.

```typescript TypeScript const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'github-profile', // Name of the profile to associate with the connection }); ```
auth = await kernel.auth.connections.create(
    domain="github.com",
    profile_name="github-profile", # Name of the profile to associate with the connection
)

2. Start a Login Session

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

Credentials are saved automatically on successful login, enabling automatic re-authentication when the session expires.

3. Poll and Submit Credentials

A single loop handles everything—initial login, 2FA, and completion:

```typescript TypeScript let state = await kernel.auth.connections.retrieve(auth.id);

while (state.flow_status === 'IN_PROGRESS') { // Submit when fields are ready (login or 2FA) if (state.flow_step === 'AWAITING_INPUT' && state.discovered_fields?.length) { const fieldValues = getCredentialsForFields(state.discovered_fields); await kernel.auth.connections.submit(auth.id, { fields: fieldValues }); }

await new Promise(r => setTimeout(r, 2000)); state = await kernel.auth.connections.retrieve(auth.id); }

if (state.status === 'AUTHENTICATED') { console.log('Authentication successful!'); }


```python Python
state = await kernel.auth.connections.retrieve(auth.id)

while state.flow_status == "IN_PROGRESS":
    # Submit when fields are ready (login or 2FA)
    if state.flow_step == "AWAITING_INPUT" and state.discovered_fields:
        field_values = get_credentials_for_fields(state.discovered_fields)
        await kernel.auth.connections.submit(auth.id, fields=field_values)
    
    await asyncio.sleep(2)
    state = await kernel.auth.connections.retrieve(auth.id)

if state.status == "AUTHENTICATED":
    print("Authentication successful!")

The discovered_fields array tells you what the login form needs:

// Example discovered_fields for login
[{ name: 'username', type: 'text' }, { name: 'password', type: 'password' }]

// Example discovered_fields for 2FA
[{ name: 'otp', type: 'code' }]

Complete Example

```typescript TypeScript import Kernel from '@onkernel/sdk';

const kernel = new Kernel();

// Create connection const auth = await kernel.auth.connections.create({ domain: 'github.com', profile_name: 'github-profile', });

const login = await kernel.auth.connections.login(auth.id);

// Single polling loop handles login + 2FA let state = await kernel.auth.connections.retrieve(auth.id);

while (state.flow_status === 'IN_PROGRESS') { if (state.flow_step === 'AWAITING_INPUT' && state.discovered_fields?.length) { // Check what fields are needed const fieldNames = state.discovered_fields.map(f => f.name);

if (fieldNames.includes('username')) {
  // Initial login
  await kernel.auth.connections.submit(auth.id, {
    fields: { username: 'my-username', password: 'my-password' }
  });
} else {
  // 2FA or additional fields
  const code = await promptUserForCode();
  await kernel.auth.connections.submit(auth.id, {
    fields: { [state.discovered_fields[0].name]: code }
  });
}

}

await new Promise(r => setTimeout(r, 2000)); state = await kernel.auth.connections.retrieve(auth.id); }

if (state.status === 'AUTHENTICATED') { console.log('Authentication successful!');

const browser = await kernel.browsers.create({ profile: { name: 'github-profile' }, stealth: true, });

// Navigate to the site—you're already logged in await page.goto('https://github.com'); }


```python Python
from kernel import Kernel
import asyncio

kernel = Kernel()

# Create connection
auth = await kernel.auth.connections.create(
    domain="github.com",
    profile_name="github-profile",
)

login = await kernel.auth.connections.login(auth.id)

# Single polling loop handles login + 2FA
state = await kernel.auth.connections.retrieve(auth.id)

while state.flow_status == "IN_PROGRESS":
    if state.flow_step == "AWAITING_INPUT" and state.discovered_fields:
        # Check what fields are needed
        field_names = [f["name"] for f in state.discovered_fields]
        
        if "username" in field_names:
            # Initial login
            await kernel.auth.connections.submit(
                auth.id,
                fields={"username": "my-username", "password": "my-password"},
            )
        else:
            # 2FA or additional fields
            code = input("Enter code: ")
            await kernel.auth.connections.submit(
                auth.id,
                fields={state.discovered_fields[0]["name"]: code},
            )

    await asyncio.sleep(2)
    state = await kernel.auth.connections.retrieve(auth.id)

if state.status == "AUTHENTICATED":
    print("Authentication successful!")
    
    browser = await kernel.browsers.create(
        profile={"name": "github-profile"},
        stealth=True,
    )
    
    # Navigate to the site—you're already logged in
    await page.goto("https://github.com")
This example covers username/password login with 2FA — the most common flow. If the site uses SSO, MFA selection, account pickers, or external actions (push notifications), see [Handling Different Input Types](#handling-different-input-types) below for how to handle each case. Every programmatic login session also has a `hosted_url`. If your flow encounters an unexpected state, you can redirect the user to this URL to complete login via the [Hosted UI](/auth/hosted-ui) instead.

Handling Different Input Types

The basic polling loop handles discovered_fields, but login pages can require other input types too.

SSO Buttons

When the login page has "Sign in with Google/GitHub/Microsoft" buttons, they appear in pending_sso_buttons:

```typescript TypeScript if (state.pending_sso_buttons?.length) { // Show the user available SSO options for (const btn of state.pending_sso_buttons) { console.log(`${btn.provider}: ${btn.label}`); }

// Submit the selected SSO button await kernel.auth.connections.submit(auth.id, { sso_button_selector: state.pending_sso_buttons[0].selector }); }


```python Python
if state.pending_sso_buttons:
    # Show the user available SSO options
    for btn in state.pending_sso_buttons:
        print(f"{btn['provider']}: {btn['label']}")
    
    # Submit the selected SSO button
    await kernel.auth.connections.submit(
        auth.id,
        sso_button_selector=state.pending_sso_buttons[0]["selector"],
    )
Common SSO provider domains (Google, Microsoft, Okta, Auth0, GitHub, etc.) are automatically allowed. For custom OAuth providers, add their domains to `allowed_domains` on the connection.

SSO Provider Selection

As an alternative to clicking an SSO button by selector, you can submit the SSO provider name directly. When SSO buttons are detected, the session state includes a sso_provider field (a string) identifying the provider that Kernel recommends. You can also specify a provider explicitly using the sso_provider submit parameter:

```typescript TypeScript if (state.pending_sso_buttons?.length) { // Submit by provider name instead of selector await kernel.auth.connections.submit(auth.id, { sso_provider: state.pending_sso_buttons[0].provider // e.g., "google" }); } ```
if state.pending_sso_buttons:
    # Submit by provider name instead of selector
    await kernel.auth.connections.submit(
        auth.id,
        sso_provider=state.pending_sso_buttons[0]["provider"],  # e.g., "google"
    )
`sso_provider` is a singular string value, not an array. Use `sso_button_selector` when you need to click a specific button by its CSS selector, and `sso_provider` when you want to identify the provider by name (e.g., `"google"`, `"microsoft"`, `"okta"`).

MFA Selection

When the site offers multiple MFA methods, they appear in mfa_options:

```typescript TypeScript if (state.mfa_options?.length) { // Available types: sms, email, totp, push, call, password, switch for (const opt of state.mfa_options) { console.log(`${opt.type}: ${opt.label}`); }

// Submit the selected MFA method await kernel.auth.connections.submit(auth.id, { mfa_option_id: 'sms' }); }


```python Python
if state.mfa_options:
    # Available types: sms, email, totp, push, call, password, switch
    for opt in state.mfa_options:
        print(f"{opt['type']}: {opt['label']}")

    # Submit the selected MFA method
    await kernel.auth.connections.submit(
        auth.id,
        mfa_option_id="sms",
    )

After selecting an MFA method, the flow continues. Poll for discovered_fields to submit the code, or handle external actions for push/security key.

The `switch` type represents generic method-switcher links like "Use another method" or "Try another way" that don't name a specific factor. Submit it the same way as any other MFA option to reveal the underlying alternatives on the next page.

Sign-In Options (Account/Org Pickers)

Some sites present non-MFA choices during login, such as account selection or organization pickers. These appear in sign_in_options as an array of objects with id, label, and optional description:

```typescript TypeScript if (state.sign_in_options?.length) { // Show available options to the user for (const opt of state.sign_in_options) { console.log(`${opt.id}: ${opt.label}`); if (opt.description) console.log(` ${opt.description}`); }

// Submit the selected option await kernel.auth.connections.submit(auth.id, { sign_in_option_id: state.sign_in_options[0].id }); }


```python Python
if state.sign_in_options:
    # Show available options to the user
    for opt in state.sign_in_options:
        print(f"{opt['id']}: {opt['label']}")
        if opt.get("description"):
            print(f"  {opt['description']}")

    # Submit the selected option
    await kernel.auth.connections.submit(
        auth.id,
        sign_in_option_id=state.sign_in_options[0]["id"],
    )
Sign-in options are distinct from MFA options. MFA options (`mfa_options`) represent second-factor authentication methods like SMS or TOTP. Sign-in options represent non-security choices like "Which account do you want to use?" or "Select your organization."

External Actions (Push, Security Key)

When the site requires an action outside the browser (push notification, security key tap), the step becomes AWAITING_EXTERNAL_ACTION:

```typescript TypeScript if (state.flow_step === 'AWAITING_EXTERNAL_ACTION') { // Show the message to the user console.log(state.external_action_message); // e.g., "Check your phone for a push notification"

// Some sites offer fallback methods alongside the external action // (e.g. "Try another way"). Submit one to switch verification methods. if (state.mfa_options?.length) { await kernel.auth.connections.submit(auth.id, { mfa_option_id: state.mfa_options[0].type, }); }

// Otherwise keep polling—the flow resumes automatically when the user completes the action }


```python Python
if state.flow_step == "AWAITING_EXTERNAL_ACTION":
    # Show the message to the user
    print(state.external_action_message)
    # e.g., "Check your phone for a push notification"

    # Some sites offer fallback methods alongside the external action
    # (e.g. "Try another way"). Submit one to switch verification methods.
    if state.mfa_options:
        await kernel.auth.connections.submit(
            auth.id,
            mfa_option_id=state.mfa_options[0]["type"],
        )

    # Otherwise keep polling—the flow resumes automatically when the user completes the action
`mfa_options`, `pending_sso_buttons`, and `sign_in_options` may be populated during `AWAITING_EXTERNAL_ACTION` when the site exposes fallback methods alongside the external action (for example, "Try another way" on a push prompt). Submit one of them to switch verification methods, or keep polling to let the user complete the external action.

Step Reference

The flow_step field indicates what the flow is waiting for:

Step Description
DISCOVERING Finding the login page and analyzing it
AWAITING_INPUT Waiting for field values, SSO button click, SSO provider selection, MFA selection, or sign-in option selection
SUBMITTING Processing submitted values
AWAITING_EXTERNAL_ACTION Waiting for push approval, security key, etc.
COMPLETED Flow has finished

Status Reference

The flow_status field indicates the current flow state:

Status Description
IN_PROGRESS Authentication is ongoing—keep polling
SUCCESS Login completed, profile saved
FAILED Login failed (check error_message)
EXPIRED Flow timed out (10 minutes for user input, 20 minutes overall)
CANCELED Flow was canceled

The status field indicates the overall connection state:

Status Description
AUTHENTICATED Profile is logged in and ready to use
NEEDS_AUTH Profile needs authentication

Updating Connections

After creating a connection, you can update its configuration with PATCH /auth/connections/{id}:

Field Description
login_url Override the login page URL
credential Update the linked credential
allowed_domains Update allowed redirect domains
health_check_interval Seconds between health checks (minimum varies by plan)
save_credentials Whether to save credentials on successful login
record_session Record a replay of every auth browser session for this connection (logins, health checks, and reauths)
proxy Proxy configuration for login sessions

Only the fields you include are updated—everything else stays the same.

```typescript TypeScript await kernel.auth.connections.update(auth.id, { login_url: 'https://example.com/login', health_check_interval: 1800, save_credentials: true, }); ```
await kernel.auth.connections.update(
    auth.id,
    login_url="https://example.com/login",
    health_check_interval=1800,
    save_credentials=True,
)

Real-Time Updates with SSE

For real-time UIs, you can stream login flow events via Server-Sent Events instead of polling:

GET /auth/connections/{id}/events

The stream delivers managed_auth_state events with the same fields as polling (flow_status, flow_step, discovered_fields, etc.) and terminates automatically when the flow reaches a terminal state.

Polling is recommended for most integrations. SSE is useful when building real-time UIs that need instant updates without polling delays.