diff --git a/agent-auth/credentials.mdx b/agent-auth/credentials.mdx index 3f33a44..83abfc1 100644 --- a/agent-auth/credentials.mdx +++ b/agent-auth/credentials.mdx @@ -1,47 +1,35 @@ --- -title: "Credentials" -description: "Securely store login credentials for automated re-authentication" +title: "Credentials Vault" --- -Credentials allow you to securely store login information that enables fully automated authentication flows. When linked to an Auth Agent, credentials enable automatic re-authentication when sessions expire—without any user interaction. +Our Credentials Vault allows you to securely store login information and automate workflows end-to-end. When combined with [Agent Auth](/agent-auth/overview), your Kernel browsers stay automatically logged in 24/7 without additional user input. -## Why Use Credentials? +## Why use Kernel's Credentials Vault? -Without stored credentials, every time a session expires, you need to redirect users back through the login flow. With credentials: +Without stored credentials, every time a browser session expires, your app needs to redirect users back through the login flow. With our credentials vault: -- **Automated re-auth** - Sessions can be refreshed automatically in the background +- **Automated re-auth** - Browsers for your AI agent stay logged in automatically - **No user interaction** - Re-authentication happens without user involvement - **Secure storage** - Credentials are encrypted at rest using per-organization keys - **Never exposed** - Values are never returned in API responses or shared with LLMs -## How Credentials Work +## Getting started - - - Create a credential with the login values for a specific domain - - - Associate the credential with an Auth Agent - - - Run one successful authentication to save form selectors - - - When sessions expire, Agent Auth uses stored credentials and selectors to re-authenticate automatically - - +### 1. Save to Credentials Vault -## Creating Credentials +When creating Agent Auth invocations, specify `save_credential_as` with a unique identifier. This then stores the credentials submitted in the Auth Agent to our vault. -Store credentials using the Credentials API. Values are encrypted immediately and cannot be retrieved. +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-netflix-login', // Save credentials when login succeeds +}); +``` + +Alternatively, you can manually store credentials to our vault. Values are encrypted immediately and cannot be retrieved in plaintext once submitted. ```typescript TypeScript -import Kernel from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Create a credential const credential = await kernel.credentials.create({ name: 'my-netflix-login', // Unique name within your org domain: 'netflix.com', // Target domain @@ -50,17 +38,10 @@ const credential = await kernel.credentials.create({ password: 'secretpassword123', }, }); - -console.log('Credential created:', credential.id); -// Note: values are NOT returned - only metadata +// Note: credential values are NEVER returned - only metadata ``` ```python Python -from kernel import Kernel - -kernel = Kernel() - -# Create a credential credential = await kernel.credentials.create( name="my-netflix-login", domain="netflix.com", @@ -69,243 +50,25 @@ credential = await kernel.credentials.create( "password": "secretpassword123", }, ) - -print(f"Credential created: {credential.id}") ``` -**Parameters:** +### 2. Automate logins -| Parameter | Required | Description | -|-----------|----------|-------------| -| `name` | Yes | Unique name for the credential within your organization | -| `domain` | Yes | Target domain this credential is for (e.g., `netflix.com`) | -| `values` | Yes | Object containing field name → value pairs | +#### `save_credential_as` +If you've stored credentials via `save_credential_as`, all subsequent browsers loaded with the associated [Auth Agent Profile](/agent-auth/hosted-ui#1-create-an-auth-agent) will be automatically logged in. We [regularly refresh](/agent-auth/faq#automatic-re-auth) the profile with the stored credentials to ensure it is valid and authenticated. - -Credential values are write-only. Once stored, they cannot be retrieved via the API. Only metadata (name, domain, created_at) is returned. - +#### `credentials.create()` +If you've manually created credentials using `credentials.create()`, you can apply them to any Agent Auth to login to a website: -## Linking Credentials to Auth Agents - -There are two ways to link credentials to an Auth Agent: - -### Option 1: Link During Auth Agent Creation ```typescript -// Create auth agent with credential link +// Initialize auth agent with specified credential const agent = await kernel.agents.auth.create({ target_domain: 'netflix.com', profile_name: 'my-profile', - credential_id: credential.id, // Link the credential -}); -``` - -### Option 2: Save Credentials During Auth Flow - -During an authentication invocation, you can save the entered credentials: - -```typescript -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'my-netflix-login', // Save credentials when login succeeds -}); -``` - -This approach: -- Creates the credential automatically when login succeeds -- Links it to the Auth Agent -- Saves the form selectors for future re-auth - -## Using Stored Credentials - -Once credentials are linked and an initial login has completed (capturing form selectors), the Auth Agent can re-authenticate automatically. - -### Checking Re-auth Capability - -```typescript -const agent = await kernel.agents.auth.retrieve(agentId); - -console.log('Status:', agent.status); // AUTHENTICATED or NEEDS_AUTH -console.log('Can Reauth:', agent.can_reauth); // true if credentials + selectors exist -console.log('Credential ID:', agent.credential_id); -console.log('Has Selectors:', agent.has_selectors); -``` - -An Auth Agent `can_reauth` when: -1. It has a linked credential (`credential_id` is set) -2. It has saved form selectors from a previous successful login (`has_selectors` is true) - -### Triggering Re-authentication - -When sessions expire, trigger re-authentication: - -```typescript -const reauth = await kernel.agents.auth.reauth(agent.id); - -switch (reauth.status) { - case 'REAUTH_STARTED': - console.log('Re-auth started, invocation:', reauth.invocation_id); - // Poll for completion - break; - case 'ALREADY_AUTHENTICATED': - console.log('Session is still valid'); - break; - case 'CANNOT_REAUTH': - console.log('Cannot re-auth:', reauth.message); - // Missing credentials or selectors - need manual login - break; -} -``` - -See [Session Monitoring](/agent-auth/session-monitoring) for automated session management. - -## Managing Credentials - -### List Credentials - -```typescript -const credentials = await kernel.credentials.list({ - domain: 'netflix.com', // Optional: filter by domain + credential_id: credential.id, // Credentials the Auth Agent should use to attempt login }); - -for (const cred of credentials) { - console.log(`${cred.name} (${cred.domain}) - Created: ${cred.created_at}`); -} -``` - -### Get Credential Details - -```typescript -const credential = await kernel.credentials.retrieve(credentialId); - -console.log('Name:', credential.name); -console.log('Domain:', credential.domain); -console.log('Created:', credential.created_at); -console.log('Updated:', credential.updated_at); -// Note: values are never returned -``` - -### Update Credentials - -```typescript -await kernel.credentials.update(credentialId, { - name: 'updated-name', // Optional: update name - values: { // Optional: update values - email: 'newemail@example.com', - password: 'newpassword', - }, -}); -``` - -### Delete Credentials - -```typescript -await kernel.credentials.delete(credentialId); -``` - - -Deleting a credential unlinks it from any associated Auth Agents. Those agents will no longer be able to re-authenticate automatically. - - -## Credential Values Schema - -The `values` object is flexible—it stores whatever key-value pairs you provide. Common patterns: - -### Basic Email/Password - -```typescript -values: { - email: 'user@example.com', - password: 'secretpassword', -} -``` - -### Username/Password - -```typescript -values: { - username: 'myusername', - password: 'secretpassword', -} -``` - -### Multiple Fields - -Some sites have additional fields (company ID, account number, etc.): - -```typescript -values: { - company_id: 'ACME123', - username: 'jsmith', - password: 'secretpassword', -} -``` - -## Complete Example: Automated Auth Flow - -Here's a complete example setting up fully automated authentication: - -```typescript -import Kernel from '@onkernel/sdk'; - -const kernel = new Kernel(); - -async function setupAutomatedAuth() { - // 1. Create credential - const credential = await kernel.credentials.create({ - name: 'acme-portal-login', - domain: 'portal.acme.com', - values: { - email: 'agent@company.com', - password: 'secure-password-123', - }, - }); - - // 2. Create auth agent with credential - const agent = await kernel.agents.auth.create({ - target_domain: 'portal.acme.com', - profile_name: 'acme-agent-profile', - credential_id: credential.id, - login_url: 'https://portal.acme.com/login', - }); - - // 3. Complete initial login to capture selectors - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - }); - - console.log('Complete initial login at:', invocation.hosted_url); - // User completes login once... - - // After initial login, agent.can_reauth will be true -} - -async function useAuthenticatedBrowser(agentId: string) { - // Check if re-auth is needed - const agent = await kernel.agents.auth.retrieve(agentId); - - if (agent.status === 'NEEDS_AUTH') { - if (agent.can_reauth) { - // Automated re-auth - const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'REAUTH_STARTED') { - // Wait for re-auth to complete - await pollForCompletion(reauth.invocation_id); - } - } else { - throw new Error('Cannot re-auth - manual login required'); - } - } - - // Use the authenticated profile - const browser = await kernel.browsers.create({ - profile: { name: agent.profile_name }, - stealth: true, - }); - - return browser; -} ``` ## Security @@ -320,6 +83,12 @@ Credentials are designed with security as the top priority: | **Never shared with LLMs** | Values are injected directly into form fields, never passed through AI models | | **Isolated execution** | Authentication runs in an isolated browser environment | + +## Notes +- Deleting a credential unlinks it from any associated Auth Agents. Those agents will no longer be able to re-authenticate automatically. +- The Credential `values` object is flexible—it stores whatever key-value pairs you provide. Common patterns include ``, ``, or even things with multiple fields like `` + + ## Best Practices 1. **Use descriptive names** - Name credentials clearly (e.g., `production-crm-login`, `staging-portal-admin`) @@ -329,14 +98,3 @@ Credentials are designed with security as the top priority: 3. **Monitor auth status** - Use [Session Monitoring](/agent-auth/session-monitoring) to detect when re-auth is needed 4. **Handle re-auth failures** - If automated re-auth fails (password changed, new 2FA method), fall back to manual login - -## Next Steps - - - - Automatically detect and handle expired sessions - - - Use the hosted UI for initial login - - diff --git a/agent-auth/early-preview.mdx b/agent-auth/early-preview.mdx deleted file mode 100644 index 42339c9..0000000 --- a/agent-auth/early-preview.mdx +++ /dev/null @@ -1,1383 +0,0 @@ ---- -title: "Early Preview" -description: "Agent Auth early preview documentation for testers" ---- - - -This is early preview documentation for Agent Auth testers. Features and APIs may change before general availability. - - -## What is Agent Auth? - -Agent Auth is an AI-powered authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It uses web agents to intelligently discover login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. - -**Key features:** - -- Automatically detects login form fields on any website -- Dynamically generates forms based on discovered fields -- Handles multi-step auth (2FA, OTP, magic links) -- Saves authenticated browser profiles for reuse -- Hourly session checks to detect when re-authentication is needed -- Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs -- Store credentials for fully automated re-authentication -- `reauth()` endpoint for programmatic session refresh - -## Choose Your Integration - -| Flow | Who enters credentials? | Best for | -|------|------------------------|----------| -| **Hosted UI** | User, in Kernel's hosted page | Simplest integration, minimal code | -| **Custom UI** | User, in YOUR app's UI | Full control over login UX | -| **Programmatic** | No one (you provide them in code) | Automated/headless, bot accounts | - -## How It Works - -1. Your backend calls `kernel.agents.auth.create()` to setup the agent, then calls `kernel.agents.auth.invocations.create()` to start the flow → Returns `invocation_id`, `hosted_url`, `handoff_code`, and `expires_at` -2. User is redirected to `hosted_url` → Enters credentials when prompted → Handles 2FA/OTP if needed → Profile saved on success -3. Your backend polls invocation status → Uses saved profile in future browser automations -4. After authentication completes, Agent Auth automatically monitors the session every hour and updates the status if re-authentication is required -5. If credentials are stored, Agent Auth can automatically re-authenticate when sessions expire - - -**Plan for re-auth from the start.** If you want automated re-authentication later, use `save_credential_as` during the initial login. This saves credentials and form selectors, enabling `can_reauth` for future sessions. - - -## Key Concepts - -### The Three Primitives - -| Concept | What it is | Lifespan | -|---------|-----------|----------| -| **Profile** | A saved browser session (cookies, localStorage). Once authenticated, use it to get logged-in browsers. | Permanent | -| **Auth Agent** | Binds a profile to a domain. Tracks auth status, credentials, and selectors. | Permanent | -| **Invocation** | A single login attempt. Created each time you need to authenticate. | 5 minutes | - -### Auth Agent - -An Auth Agent is a persistent entity that manages authentication for a specific domain + profile combination. You create or retrieve Auth Agents using `kernel.agents.auth.create()`. This method is idempotent—if an agent already exists for the domain and profile, it returns the existing one. - -The Auth Agent stores: - -- The target domain (e.g., `doordash.com`) -- The linked browser profile -- The login URL (learned during discovery, used to speed up future invocations) -- The auth check URL (where to verify logged-in state) -- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) -- Linked credential ID for automated re-auth -- Saved form selectors for deterministic re-auth - -**Auth Agent fields:** - -| Field | Description | -|-------|-------------| -| `id` | Unique identifier for the agent | -| `status` | `AUTHENTICATED` or `NEEDS_AUTH` | -| `credential_id` | ID of linked credential (if any) | -| `credential_name` | Name of linked credential (if any) | -| `can_reauth` | `true` if agent has credentials AND selectors for automated re-auth | -| `has_selectors` | `true` if form selectors were saved from a previous login | -| `last_auth_check_at` | When the last session health check was performed | - - -**When is `can_reauth` true?** The agent must have BOTH: -1. A linked credential (`credential_id` is set) -2. Saved form selectors from a previous successful login (`has_selectors` is true) - -If you want automated re-auth later, use `save_credential_as` during your first login flow. - - -### Auth Agent Invocation - -An Invocation is a single authentication attempt that belongs to an Auth Agent. You start a new Invocation by calling `kernel.agents.auth.invocations.create(agent.id)`. Over time, an Auth Agent accumulates multiple Invocations as sessions need to be established or refreshed. - -**Invocation expires in 5 minutes.** If the user doesn't complete login in time, the invocation status becomes `EXPIRED`. - -**Invocation Types:** - -- `login` - Authentication flow (used for both first-time login and re-authentication) -- `step_up` - Real-time authentication challenge (coming soon) - -### Status Reference - -Different endpoints return different status values. Here's the complete reference: - -**Invocation status** (from `invocations.retrieve()`): - -| Status | Description | -|--------|-------------| -| `IN_PROGRESS` | Invocation is active, user is completing login | -| `SUCCESS` | Login completed successfully, profile is saved | -| `EXPIRED` | Invocation timed out (5 minutes) | -| `CANCELED` | Invocation was explicitly canceled | - -**Invocation create response** (from `invocations.create()`): - -| Status | Description | -|--------|-------------| -| `INVOCATION_CREATED` | New invocation started, redirect user to `hosted_url` | -| `ALREADY_AUTHENTICATED` | Profile is already logged in, no invocation needed | - -**Auth Agent status** (from `agents.auth.retrieve()`): - -| Status | Description | -|--------|-------------| -| `AUTHENTICATED` | Profile has a valid logged-in session | -| `NEEDS_AUTH` | Session expired or never logged in | - -**Reauth response** (from `agents.auth.reauth()`): - -| Status | Description | -|--------|-------------| -| `REAUTH_STARTED` | Automated re-auth started, poll `invocation_id` for completion | -| `ALREADY_AUTHENTICATED` | Session is still valid, no action needed | -| `CANNOT_REAUTH` | Missing credentials or selectors, manual login required | - -### Automatic Session Monitoring - -Auth Agents with `AUTHENTICATED` status are automatically checked hourly to verify the session is still valid. If the check detects the user has been logged out (session expired, cookies cleared by the site, etc.), the Auth Agent status is updated to `NEEDS_AUTH`. - -**How to use this:** - -- Poll `kernel.agents.auth.retrieve(id)` periodically to check if re-authentication is needed -- When status changes from `AUTHENTICATED` to `NEEDS_AUTH`: - - If `can_reauth` is `true`: Call `kernel.agents.auth.reauth(id)` for automated re-auth - - Otherwise: Trigger a new auth flow with `kernel.agents.auth.invocations.create(agent.id)` -- The check runs passively and doesn't modify the saved profile - -### What Discover Does - -When you call the discover endpoint on an Invocation, the Auth Agent: - -1. Navigates to the target domain (or directly to `login_url` if provided) -2. Finds the login page by looking for sign-in links/buttons -3. Extracts all form input fields on the page (email, password, etc.) -4. Returns the discovered fields with their names, types, labels, and selectors - -If the profile is already logged in from a previous session, discover returns `logged_in: true`. At that point, discovery is complete, no fields will be returned, and you should simply proceed to use the associated profile. No credentials or further steps are required. - -### What Submit Does - -When you call the submit endpoint on an Invocation, the Auth Agent: - -1. Fills in the form fields using the `field_values` you provide (matched by field name) -2. Clicks the submit button -3. Waits for the page to respond -4. Analyzes what happened next - -The AI determines the outcome by looking at the page after submission: - -- **Logged in** - The page shows a logged-in state (dashboard, account page, etc.) → Returns `logged_in: true` -- **Error displayed** - The page shows an error message → Returns `success: false` with `error_message` -- **New fields appeared** - The page now shows different form fields (e.g., OTP input after email/password) → Returns `needs_additional_auth: true` with `additional_fields` - -When you get `additional_fields`, collect those values from the user and call submit again. This loop continues until you get `logged_in: true` or an error. - ---- - -## Tips for Early Preview Testing - -- **Start simple** - Use the Hosted UI (Option 1) and test with sites that have basic email/password login -- **Provide login_url when known** - Speeds up discovery by skipping the login page search -- **Handle 2FA** - Test flows with 2FA to ensure your integration handles `additional_fields` -- **Poll with backoff** - Start at 2s intervals, increase to 5s. Max timeout is 5 minutes. See polling example below. -- **Clean up profiles** - Delete test profiles with `kernel.profiles.delete(profileName)` when done to avoid clutter -- **Plan for re-auth** - Use `save_credential_as` during initial login to enable automated re-auth later -- **Don't log credentials** - Never log `field_values` or credential data - ---- - -## Support - -Questions or issues? Reach out to us on Slack! - ---- - -## Installation (Early Preview) - -Before using Agent Auth, install the early preview SDK: - -**TypeScript/Node.js:** - -```json -{ - "dependencies": { - "@onkernel/sdk": "https://pkg.stainless.com/s/kernel-typescript/5ace6e2da99e73ad2d863be786213bb9fd28ce53/dist.tar.gz" - } -} -``` - -**Python (requirements.txt):** - -``` -kernel @ https://pkg.stainless.com/s/kernel-python/e7cab450cadd635e0d94db9618e4d729f5d97bcf/kernel-0.23.0-py3-none-any.whl -``` - -Or in pyproject.toml: - -```toml -[project] -dependencies = [ - "kernel @ https://pkg.stainless.com/s/kernel-python/e7cab450cadd635e0d94db9618e4d729f5d97bcf/kernel-0.23.0-py3-none-any.whl" -] -``` - ---- - -## Integration Options - - -**Quick Start Examples:** We have standalone example scripts for each integration pattern: -- **Developer Pre-creates Credential** - When you already have user credentials stored -- **Hosted UI Flow** - Redirect users to complete login themselves -- **Programmatic Flow** - Full control over the login process - - -### Option 1: Hosted UI (Recommended) - -The simplest approach—redirect users to our hosted authentication UI, then poll for completion. - -#### Happy Path in 30 Seconds - -```typescript -// 1. Create agent + invocation -const agent = await kernel.agents.auth.create({ target_domain: 'example.com', profile_name: 'user-123' }); -const invocation = await kernel.agents.auth.invocations.create({ auth_agent_id: agent.id }); - -// 2. Redirect user (or skip if ALREADY_AUTHENTICATED) -if (invocation.status !== 'ALREADY_AUTHENTICATED') { - redirect(invocation.hosted_url); -} - -// 3. Poll for completion, then use the profile -const browser = await kernel.browsers.create({ profile: { name: 'user-123' }, stealth: true }); -``` - -#### Hosted UI Redirect Contract - -| Question | Answer | -|----------|--------| -| Is `hosted_url` single-use? | No, user can refresh the page | -| Does Kernel redirect back to my app? | No, you must poll for completion | -| How do I detect user abandoned? | Invocation status becomes `EXPIRED` after 5 minutes | -| Can I pass state/return_url? | Not currently supported | - -#### Step 1: Start the Auth Flow - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Step 1: Create or Find Auth Agent -const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', - profile_name: 'netflix-user-123', - login_url: 'https://netflix.com/login', // Optional: speeds up discovery - proxy: { proxy_id: 'proxy_abc123' }, // Optional: use a proxy -}); - -// Step 2: Start the Auth Flow -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'netflix-creds', // Save credentials for future re-auth -}); - -// invocation returns: status, invocation_id, hosted_url, handoff_code, expires_at -``` - -#### Step 2: Redirect User - -```typescript -// Check if already logged in -if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Already logged in! Profile is ready to use.'); - // Skip to Step 4 - Use the Profile -} else { - // Redirect to hosted flow - window.location.href = invocation.hosted_url; -} -``` - -#### Step 3: Poll for Completion - -```typescript -// Recommended: Poll with backoff (2s → 3s → 5s), max 5 minutes -async function pollForCompletion(invocationId: string) { - const maxWaitMs = 5 * 60 * 1000; // 5 minutes - const start = Date.now(); - let delay = 2000; - - while (Date.now() - start < maxWaitMs) { - const status = await kernel.agents.auth.invocations.retrieve(invocationId); - - if (status.status === 'SUCCESS') { - return { success: true, status }; - } - if (status.status === 'EXPIRED' || status.status === 'CANCELED') { - return { success: false, reason: status.status }; - } - - await new Promise(r => setTimeout(r, delay)); - delay = Math.min(delay * 1.5, 5000); // Cap at 5s - } - - return { success: false, reason: 'TIMEOUT' }; -} - -const result = await pollForCompletion(invocation.invocation_id); - -if (result.success) { - console.log('Success! Profile is ready.'); - - // Verify auth agent status - const authAgent = await kernel.agents.auth.retrieve(agent.id); - console.log(authAgent.status); // "AUTHENTICATED" - console.log(authAgent.can_reauth); // true if credentials were saved -} -``` - - -**Don't poll too aggressively.** Use exponential backoff starting at 2 seconds. Invocations typically complete in 10-60 seconds depending on the site and user speed. - - -#### Step 4: Use the Profile - -```typescript -// Create a browser with a saved profile - the user is already logged in! -const browser = await kernel.browsers.create({ - stealth: true, - profile: { - name: 'netflix-user-123', // Use the profile name from Step 1 - }, - proxy_id: 'proxy_abc123', // If you used a proxy in Step 1, use the same one here -}); - -// browser.session_id - Session ID -// browser.cdp_ws_url - CDP URL for Playwright/Puppeteer -// Run your automation - the browser is already authenticated! - -// Cleanup when done -await kernel.browsers.deleteByID(browser.session_id); -``` - - -Use `stealth: true` and the same proxy configuration you passed to `kernel.agents.auth.create()`. Agent Auth runs the authentication browser session with stealth mode enabled—mismatched settings may cause unexpected behavior with bot detection or session issues. - - -#### Step 5: Handle Re-authentication - -When sessions expire, check if automated re-auth is available: - -```typescript -// Check auth agent status -const agent = await kernel.agents.auth.retrieve(agentId); - -if (agent.status === 'NEEDS_AUTH') { - if (agent.can_reauth) { - // Automated re-auth (no user interaction needed) - const reauth = await kernel.agents.auth.reauth(agent.id); - - if (reauth.status === 'REAUTH_STARTED') { - // Poll for completion using the same backoff pattern - const result = await pollForCompletion(reauth.invocation_id); - console.log('Re-auth complete:', result); - } else if (reauth.status === 'ALREADY_AUTHENTICATED') { - console.log('Session is still valid'); - } else if (reauth.status === 'CANNOT_REAUTH') { - // Selectors may have drifted - fall back to manual login - console.log('Cannot reauth:', reauth.message); - } - } else { - // Manual re-auth required - redirect user to hosted UI - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'netflix-creds', // Save credentials this time! - }); - window.location.href = invocation.hosted_url; - } -} -``` - - -**If reauth fails:** The site's login form may have changed (selectors drifted). Fall back to creating a new invocation for manual login—this will re-capture the updated selectors. - - ---- - -### Option 2: Custom UI with Discover/Submit APIs - -Build your own authentication UI using the lower-level discover and submit APIs. You control the UI, but need to handle the multi-step flow yourself. - -#### When to Use Custom UI - -- You want login to match your app's design -- You need to collect credentials in your own form -- You're building a native mobile app - -#### Step 1: Start the Auth Invocation - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Step 1: Create Agent & Start Invocation -const agent = await kernel.agents.auth.create({ - target_domain: 'doordash.com', - profile_name: 'my-profile-123', -}); - -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'doordash-creds', // Optional: save for re-auth -}); -``` - -#### Step 2: Exchange Handoff Code for JWT - -```typescript -const exchangeResponse = await kernel.agents.auth.invocations.exchange( - invocation.invocation_id, - { code: invocation.handoff_code } -); - -const jwt = exchangeResponse.jwt; // 30 min TTL -``` - -#### Step 3: Create JWT-Authenticated Client - -```typescript -const jwtKernel = new Kernel({ apiKey: jwt }); -``` - -#### Step 4: Discover Login Fields - -```typescript -const discoverResponse = await jwtKernel.agents.auth.invocations.discover( - invocation.invocation_id, - {} // Optional: { login_url: 'https://example.com/login' } -); - -if (discoverResponse.logged_in) { - // Already logged in! Profile saved. Skip to using the profile. -} - -if (discoverResponse.success && discoverResponse.fields) { - // Render these fields in your UI - // Fields are returned in DOM order—display them as-is -} -``` - - -**Discover is idempotent.** If the user refreshes or you call discover again, it returns the cached fields from the same invocation. You don't need to store fields—just call discover again to resume. - - -**About discovered fields:** - -- Fields are returned in DOM order from the login page -- Currently supports: `text`, `email`, `password`, `tel`, `number`, `code` -- Does NOT include CAPTCHAs, SSO buttons, or "remember me" checkboxes -- `additional_fields` (from submit) may include 2FA codes, security questions, or "select account" prompts—treat them generically as "more fields to fill" - -#### Step 5: Map Discovered Fields to Credentials - -The discover response returns an array of fields. Each field has a `name` property that you use as the key when submitting credentials. - -**Example discover response:** - -```json -{ - "success": true, - "fields": [ - { "name": "email", "type": "email", "label": "Email Address" }, - { "name": "password", "type": "password", "label": "Password" } - ] -} -``` - -**To submit, use each field's `name` as the key in `field_values`:** - -```json -{ - "field_values": { - "email": "user@example.com", - "password": "secretpassword" - } -} -``` - -**Recommended mapping strategy:** - -```typescript -// Mapping priority: -// 1. Match by field.type (most reliable) -// 2. Match by field.name patterns (fallback) -// 3. Log unmapped fields for debugging - -function mapCredentialsToFields( - fields: Array<{ name: string; type: string }>, - credentials: { email: string; password: string; code?: string } -): Record { - const fieldValues: Record = {}; - const unmapped: string[] = []; - - for (const field of fields) { - const name = field.name.toLowerCase(); - const type = field.type.toLowerCase(); - - // Priority 1: Match by type - if (type === 'email') { - fieldValues[field.name] = credentials.email; - } else if (type === 'password') { - fieldValues[field.name] = credentials.password; - } else if (type === 'code') { - fieldValues[field.name] = credentials.code || ''; - } - // Priority 2: Match by name patterns - else if (name.includes('email') || name.includes('user')) { - fieldValues[field.name] = credentials.email; - } else if (name.includes('pass')) { - fieldValues[field.name] = credentials.password; - } else { - unmapped.push(field.name); - } - } - - // Log unmapped fields for debugging (but never log actual values!) - if (unmapped.length > 0) { - console.warn('Unmapped fields:', unmapped); - } - - return fieldValues; -} - -const fieldValues = mapCredentialsToFields(discoverResponse.fields, userCredentials); -``` - - -**Never log `field_values` or credential data.** Only log field names for debugging. - - -#### Step 6: Submit Credentials (Loop Until Logged In) - -```typescript -let submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: fieldValues } -); - -// Handle multi-step auth flows -while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { - // Map the additional fields the same way - const additionalValues: Record = {}; - - for (const field of submitResponse.additional_fields) { - if (field.type === 'code' || field.name.includes('code') || field.name.includes('otp')) { - // Prompt user for their 2FA code - additionalValues[field.name] = await promptUserForOTP(); - } else if (field.type === 'password' || field.name.includes('password')) { - additionalValues[field.name] = userCredentials.password; - } - // Add more mappings as needed - } - - // Submit again with the new fields - submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: additionalValues } - ); -} - -// Check final result -if (submitResponse.logged_in) { - // Success! Profile saved. -} - -if (submitResponse.error_message) { - // Login failed, show error to user - console.error(submitResponse.error_message); -} -``` - -**Field mapping tips:** - -- Always use `field.name` as the key in `field_values`—this is what the agent uses to match values to form inputs -- Check both `field.type` and `field.name` for matching—some sites use generic types but descriptive names -- Common patterns: `type: "email"` → user's email, `type: "password"` → user's password, `type: "code"` → 2FA/OTP code -- For `additional_fields` with type `code`, you'll typically need to prompt the user in real-time for their 2FA code - ---- - -### Option 3: Programmatic (Headless/Automated) - -For bots, service accounts, or automated testing where you provide credentials in code and no user is involved. - -#### When to Use Programmatic - -- Bot or service accounts with known credentials -- Automated testing/CI pipelines -- Backend-only flows with no user interaction - -#### Key Differences from Custom UI - -- You provide credentials from environment variables or a secrets manager -- No user prompt for 2FA—you need a TOTP generator or alternate method -- Use API key auth directly (no JWT exchange needed) - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// Credentials from your secrets manager -const CREDS = { - email: process.env.BOT_EMAIL!, - password: process.env.BOT_PASSWORD!, -}; - -// 1. Create agent + invocation -const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', - profile_name: 'bot-profile', - login_url: 'https://example.com/login', // Recommended for bots -}); - -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'bot-creds', // Enable reauth -}); - -if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Already logged in!'); -} else { - // 2. Discover fields (API key auth works, no JWT needed) - const discover = await kernel.agents.auth.invocations.discover( - invocation.invocation_id, - {} - ); - - if (discover.logged_in) { - console.log('Already logged in during discover!'); - } else if (discover.fields) { - // 3. Map credentials to fields - const fieldValues = mapCredentialsToFields(discover.fields, CREDS); - - // 4. Submit - const result = await kernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: fieldValues } - ); - - if (result.logged_in) { - console.log('Login successful!'); - } else if (result.needs_additional_auth) { - // Handle 2FA - see note below - console.log('2FA required:', result.additional_fields); - } - } -} - -// 5. Use the profile -const browser = await kernel.browsers.create({ - profile: { name: 'bot-profile' }, - stealth: true, -}); -``` - - -**2FA with bots:** If the target site requires 2FA, you'll need one of: -- A TOTP generator library (for authenticator app codes) -- Access to the account's email API (for email codes) -- App-specific passwords (if the service supports them) -- A service account without 2FA - -If you can't automate 2FA, the programmatic flow won't work for that site. - - ---- - -## End-to-End Example: The Complete Lifecycle - -Here's a complete example showing the full lifecycle: initial auth, using the profile, detecting session expiry, and re-auth. - -```typescript -import { Kernel } from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// === INITIAL SETUP (run once per user) === -async function setupAuth(userId: string, targetDomain: string) { - const profileName = `user-${userId}-${targetDomain}`; - - // 1. Create agent - const agent = await kernel.agents.auth.create({ - target_domain: targetDomain, - profile_name: profileName, - }); - - // 2. Start auth flow - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: `${profileName}-creds`, // Enable future reauth! - }); - - if (invocation.status === 'ALREADY_AUTHENTICATED') { - return { agentId: agent.id, profileName, ready: true }; - } - - // 3. Return hosted URL for user to complete login - return { - agentId: agent.id, - profileName, - ready: false, - hostedUrl: invocation.hosted_url, - invocationId: invocation.invocation_id, - }; -} - -// === USE THE PROFILE (run for each automation) === -async function runAutomation(agentId: string, profileName: string) { - // 1. Check if we need to re-auth - const agent = await kernel.agents.auth.retrieve(agentId); - - if (agent.status === 'NEEDS_AUTH') { - if (agent.can_reauth) { - // Automated re-auth - const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'REAUTH_STARTED') { - await pollForCompletion(reauth.invocation_id); - } - } else { - throw new Error('Session expired and cannot reauth - user must log in again'); - } - } - - // 2. Create browser with authenticated profile - const browser = await kernel.browsers.create({ - profile: { name: profileName }, - stealth: true, - }); - - try { - // 3. Run your automation - // const page = await connectPlaywright(browser.cdp_ws_url); - // await page.goto('https://example.com/dashboard'); - // ... do your work ... - - return { success: true }; - } finally { - // 4. Always clean up - await kernel.browsers.deleteByID(browser.session_id); - } -} - -// === HELPER === -async function pollForCompletion(invocationId: string) { - const maxWaitMs = 5 * 60 * 1000; - const start = Date.now(); - let delay = 2000; - - while (Date.now() - start < maxWaitMs) { - const status = await kernel.agents.auth.invocations.retrieve(invocationId); - if (status.status !== 'IN_PROGRESS') return status; - await new Promise(r => setTimeout(r, delay)); - delay = Math.min(delay * 1.5, 5000); - } - throw new Error('Timeout waiting for auth'); -} -``` - -**What to store in your database:** - -| Data | Permanent? | Purpose | -|------|------------|---------| -| `agentId` | Yes | For reauth, status checks | -| `profileName` | Yes | For creating browsers | -| `invocationId` | No (5 min TTL) | Only while polling for completion | - ---- - -## Credentials API - -Store credentials for fully automated re-authentication. Credentials are encrypted at rest and never exposed in API responses. - -### Create Credential - -```typescript -const credential = await kernel.credentials.create({ - name: 'my-netflix-login', - domain: 'netflix.com', - values: { - email: 'user@example.com', - password: 'secretpassword', - }, -}); -``` - -### Link Credential to Auth Agent - -```typescript -const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', - profile_name: 'my-profile', - credential_name: credential.name, // Link the credential by name -}); -``` - -### List Credentials - -```typescript -const response = await kernel.credentials.list({ - domain: 'netflix.com', // Optional filter -}); - -// Response is paginated - access items via .items -for (const credential of response.items) { - console.log(credential.id, credential.name); -} -``` - -### Delete Credential - -```typescript -await kernel.credentials.delete(credentialId); -``` - ---- - -## Reauth API - -Trigger automated re-authentication when sessions expire. - -```typescript -// Check if re-auth is needed -const agent = await kernel.agents.auth.retrieve(agentId); - -if (agent.status === 'NEEDS_AUTH' && agent.can_reauth) { - const reauth = await kernel.agents.auth.reauth(agent.id); - - switch (reauth.status) { - case 'REAUTH_STARTED': - console.log('Re-auth in progress:', reauth.invocation_id); - // Poll for completion... - break; - case 'ALREADY_AUTHENTICATED': - console.log('Session is still valid'); - break; - case 'CANNOT_REAUTH': - console.log('Cannot reauth:', reauth.message); - // Fall back to manual login - break; - } -} -``` - -**Requirements for `can_reauth`:** - -- Auth Agent must have a linked credential (`credential_id`) -- Auth Agent must have saved form selectors (`has_selectors`) from a previous successful login - ---- - -## API Reference - -### Auth Agents - -#### POST /agents/auth - -Create or find an auth agent for a domain/profile combination. This is idempotent—calling with the same domain and profile returns the existing agent. - -**Auth:** API key - -**Request:** - -```json -{ - "target_domain": "netflix.com", - "profile_name": "netflix-user-123", - "login_url": "https://netflix.com/login", - "credential_name": "my-netflix-login", - "proxy": { - "proxy_id": "proxy_abc123" - } -} -``` - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `target_domain` | Yes | Target domain for authentication | -| `profile_name` | Yes | Name of the profile to use | -| `login_url` | No | Login page URL to skip discovery | -| `credential_name` | No | Name of an existing credential to link for auto-fill and re-auth | -| `proxy` | No | Proxy configuration | - -**Response:** - -```json -{ - "id": "abc123", - "profile_name": "netflix-user-123", - "domain": "netflix.com", - "status": "NEEDS_AUTH", - "credential_id": "cred_abc123", - "credential_name": "my-netflix-login", - "can_reauth": false, - "has_selectors": false, - "last_auth_check_at": null -} -``` - -#### GET /agents/auth/{id} - -Get auth agent details and current authentication status. - -**Auth:** API key - -**Response:** - -```json -{ - "id": "abc123", - "profile_name": "netflix-user-123", - "domain": "netflix.com", - "status": "AUTHENTICATED", - "credential_id": "cred_abc123", - "credential_name": "my-netflix-login", - "can_reauth": true, - "has_selectors": true, - "last_auth_check_at": "2025-01-15T10:30:00Z" -} -``` - -#### DELETE /agents/auth/{id} - -Delete an auth agent. - -**Auth:** API key - -**Response:** 204 No Content - - -Deleting an auth agent does not delete the associated browser profile. Use `kernel.profiles.delete(profileName)` to clean up profiles separately. - - -#### GET /agents/auth - -List auth agents with optional filters. - -**Auth:** API key - -**Query Parameters:** - -| Parameter | Description | -|-----------|-------------| -| `profile_name` | Filter by profile name | -| `target_domain` | Filter by target domain | -| `limit` | Maximum results (default: 20, max: 100) | -| `offset` | Number of results to skip | - -**Response:** - -The SDK returns a paginated response object. Access items via the `items` property: - -```typescript -const response = await kernel.agents.auth.list({ target_domain: 'netflix.com' }); - -for (const agent of response.items) { - console.log(agent.id, agent.status); -} -``` - -**Raw API Response:** - -```json -[ - { - "id": "abc123", - "profile_name": "netflix-user-123", - "domain": "netflix.com", - "status": "AUTHENTICATED", - "credential_id": "cred_abc123", - "credential_name": "my-netflix-login", - "has_selectors": true, - "can_reauth": true, - "last_auth_check_at": "2025-01-15T10:30:00Z" - } -] -``` - -**Headers:** -- `X-Has-More`: Whether there are more results -- `X-Next-Offset`: Offset for next page - -#### POST /agents/auth/{id}/reauth - -Trigger automated re-authentication. - -**Auth:** API key - -**Response (reauth started):** - -```json -{ - "status": "REAUTH_STARTED", - "invocation_id": "inv_xyz789" -} -``` - -**Response (already authenticated):** - -```json -{ - "status": "ALREADY_AUTHENTICATED" -} -``` - -**Response (cannot reauth):** - -```json -{ - "status": "CANNOT_REAUTH", - "message": "Missing stored credential or form selectors" -} -``` - ---- - -### Auth Agent Invocations - -#### POST /agents/auth/invocations - -Create an invocation to start an auth flow for an existing auth agent. - -**Auth:** API key - -**Request:** - -```json -{ - "auth_agent_id": "abc123", - "save_credential_as": "my-saved-creds" -} -``` - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `auth_agent_id` | Yes | ID of the auth agent | -| `save_credential_as` | No | Save credentials with this name for future re-auth | - -**Response (invocation created):** - -```json -{ - "status": "INVOCATION_CREATED", - "invocation_id": "inv_xyz789", - "handoff_code": "aBcD123EfGh456IjKl789", - "hosted_url": "https://agent-auth.onkernel.com/agents/auth/invocations/inv_xyz789?code=aBcD123", - "expires_at": "2025-12-01T12:00:00Z" -} -``` - -**Response (already logged in - no invocation needed):** - -```json -{ - "status": "ALREADY_AUTHENTICATED" -} -``` - - -When `status` is `ALREADY_AUTHENTICATED`, the agent is already logged in and no invocation was created. You can proceed directly to using the profile. - - -#### GET /agents/auth/invocations/{invocation_id} - -Get invocation status and details. - -**Auth:** API key or JWT (from exchange) - -**Response:** - -```json -{ - "status": "IN_PROGRESS", - "app_name": "My App", - "target_domain": "doordash.com", - "expires_at": "2025-12-01T12:00:00Z" -} -``` - -| Status | Description | -|--------|-------------| -| `IN_PROGRESS` | User is completing login | -| `SUCCESS` | Login successful, profile saved | -| `EXPIRED` | Invocation timed out | -| `CANCELED` | Invocation was canceled | - -#### POST /agents/auth/invocations/{invocation_id}/exchange - -Exchange a handoff code for a JWT token. - -**Auth:** None (handoff code is the credential) - -**Request:** - -```json -{ - "code": "aBcD123EfGh456IjKl789" -} -``` - -**Response:** - -```json -{ - "invocation_id": "inv_xyz789", - "jwt": "eyJhbGciOiJIUzI1NiIs..." -} -``` - -#### POST /agents/auth/invocations/{invocation_id}/discover - -Discover login form fields on the target site. - -**Auth:** API key or JWT (from exchange) - -**Request:** - -```json -{ - "login_url": "https://doordash.com/login" -} -``` - -| Parameter | Description | -|-----------|-------------| -| `login_url` | Override the stored login URL | - -**Response (fields found):** - -```json -{ - "success": true, - "logged_in": false, - "login_url": "https://identity.doordash.com/auth", - "page_title": "Sign In - DoorDash", - "fields": [ - { - "name": "email", - "type": "email", - "label": "Email", - "placeholder": "Enter your email", - "required": true, - "selector": "//input[@id='email']" - }, - { - "name": "password", - "type": "password", - "label": "Password", - "required": true, - "selector": "//input[@id='password']" - } - ] -} -``` - -**Response (already logged in):** - -```json -{ - "success": true, - "logged_in": true -} -``` - -#### POST /agents/auth/invocations/{invocation_id}/submit - -Submit credentials and attempt login. - -**Auth:** API key or JWT (from exchange) - -**Request:** - -```json -{ - "field_values": { - "email": "user@example.com", - "password": "********" - } -} -``` - -**Response (success):** - -```json -{ - "success": true, - "logged_in": true, - "app_name": "My App", - "target_domain": "doordash.com" -} -``` - -**Response (needs 2FA):** - -```json -{ - "success": true, - "logged_in": false, - "needs_additional_auth": true, - "additional_fields": [ - { - "name": "code", - "type": "code", - "label": "Verification Code", - "required": true, - "selector": "//input[@name='code']" - } - ] -} -``` - -**Response (error):** - -```json -{ - "success": false, - "logged_in": false, - "error_message": "Incorrect email or password" -} -``` - ---- - -### Credentials - -#### POST /credentials - -Create a new credential. - -**Auth:** API key - -**Request:** - -```json -{ - "name": "my-netflix-login", - "domain": "netflix.com", - "values": { - "email": "user@example.com", - "password": "secretpassword" - } -} -``` - -| Parameter | Required | Description | -|-----------|----------|-------------| -| `name` | Yes | Unique name within your organization | -| `domain` | Yes | Target domain | -| `values` | Yes | Key-value pairs of credential fields | - -**Response:** - -```json -{ - "id": "cred_abc123", - "name": "my-netflix-login", - "domain": "netflix.com", - "created_at": "2025-01-15T10:30:00Z", - "updated_at": "2025-01-15T10:30:00Z" -} -``` - - -Credential values are write-only and never returned in API responses. - - -#### GET /credentials - -List credentials. - -**Auth:** API key - -**Query Parameters:** - -| Parameter | Description | -|-----------|-------------| -| `domain` | Filter by domain | -| `limit` | Maximum results (default: 20, max: 100) | -| `offset` | Number of results to skip | - -#### GET /credentials/{id} - -Get credential metadata (values not returned). - -#### PATCH /credentials/{id} - -Update a credential. - -**Request:** - -```json -{ - "name": "updated-name", - "values": { - "email": "newemail@example.com", - "password": "newpassword" - } -} -``` - -#### DELETE /credentials/{id} - -Delete a credential. - ---- - -### Field Types - -| Type | Description | -|------|-------------| -| `text` | Generic text input | -| `email` | Email address | -| `password` | Password (masked) | -| `tel` | Phone number | -| `number` | Numeric input | -| `url` | URL input | -| `code` | Verification code (OTP, 2FA) | - ---- - -## Security - -| Feature | Description | -|---------|-------------| -| **Credentials encrypted at rest** | All credential values are encrypted using per-organization keys | -| **Credentials never exposed** | Values are write-only and never returned in API responses | -| **Credentials never shared with LLMs** | Values are submitted programmatically directly to the target site | -| **Short-lived tokens** | JWT tokens expire after 30 minutes | -| **Handoff codes expire** | One-time use, expire after 5 minutes | -| **Profile encryption** | Browser profiles are encrypted at rest | -| **Isolated execution** | Each auth flow runs in an isolated browser environment | - -### FAQ - -**Does Kernel see my users' credentials?** - -Yes, when you use `submit()`, credentials pass through Kernel's servers to fill the form on the target site. They are: -- Encrypted in transit (TLS) -- Never logged or persisted beyond the immediate use -- Never passed to AI/LLM models -- Used only to fill form fields and click submit - -**Can I delete or rotate credentials?** - -Yes. Use `kernel.credentials.delete(id)` to delete, or `kernel.credentials.update(id, { values: {...} })` to rotate. - -**What if `save_credential_as` captures the wrong fields?** - -The credential captures whatever `field_values` you submit. If you submit extra fields or wrong mappings, those get saved. You can update or delete the credential and try again. - -**Should I log `field_values` for debugging?** - -Never log credential values. Log only field names if you need to debug mapping issues. diff --git a/agent-auth/faq.mdx b/agent-auth/faq.mdx new file mode 100644 index 0000000..9d32926 --- /dev/null +++ b/agent-auth/faq.mdx @@ -0,0 +1,119 @@ +--- +title: "FAQ" +--- + +## Key Concepts + +### Auth Agent + +An **Auth Agent** is a persistent entity that manages authentication for a specific domain + [profile](/browsers/profiles) combination. It stores: + +- The target domain (e.g., `netflix.com`) +- The linked browser profile +- The login URL (learned during discovery) +- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) +- Optional linked credentials for automated re-auth + +Auth Agents are **idempotent**—if an agent already exists for the domain and profile, `kernel.agents.auth.create()` returns the existing one. + +### Invocation + +An **Invocation** is a single authentication attempt. You start one by calling `kernel.agents.auth.invocations.create()`. Over time, an Auth Agent accumulates multiple invocations as sessions need to be established or refreshed. + +**Invocation statuses:** +- `IN_PROGRESS` - User is completing the auth flow +- `SUCCESS` - Authentication completed, profile saved +- `EXPIRED` - Invocation timed out (5 minutes) +- `CANCELED` - Invocation was explicitly canceled + +### Profile + +A **Profile** stores browser session state (cookies, localStorage) that persists across browser sessions. Once authenticated via Agent Auth, use the profile with `kernel.browsers.create()` to get an already-logged-in browser. + +See [Profiles](/browsers/profiles) for more details on working with browser profiles. + +### Credentials + +**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed to LLMs or API responses. + +See [Credentials](/agent-auth/credentials) for details on storing and using credentials. + + +## Using Proxies with Agent Auth + +If the target site requires a specific IP or region, configure the same proxy when initializing the Auth Agent and subsequent browsers: + +```typescript +// Create auth agent with proxy +const agent = await kernel.agents.auth.create({ + target_domain: 'region-locked-site.com', + profile_name: 'my-profile', + proxy: { proxy_id: 'proxy_abc123' }, +}); + +// Use the same proxy when creating browsers +const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + proxy_id: 'proxy_abc123', + stealth: true, +}); +``` + + +Use the same proxy configuration for both the Auth Agent and subsequent browser sessions. Different IPs may trigger security measures on the target site. + + +## Automatic Re-Auth + +Auth Agents with `AUTHENTICATED` status are automatically checked hourly to verify each profile session is still valid. The check: + +1. Opens a Kernel browser with the authenticated profile +2. Navigates to the auth check URL (usually the main site) +3. Determines if the user is still logged in +4. Updates the Auth Agent status if the session has expired + +If you've set `save_credential_as` when you created the Agent Auth invocation, you don't need to do anything for your browsers to stay logged in. We'll automatically refresh the associated profiles whenever we detect that the authentication stored has expired. + +If you chose not to opt into `save_credentials_as`, you'll need to implement polling logic to detect when your profile has expired. Then, re-auth: + + +```typescript TypeScript +import Kernel from '@onkernel/sdk'; + +const kernel = new Kernel(); + +async function checkAndRefreshAuth(agentId: string) { + const agent = await kernel.agents.auth.retrieve(agentId); + + console.log('Status:', agent.status); + console.log('Last check:', agent.last_auth_check_at); + + if (agent.status === 'NEEDS_AUTH') { + console.log('Session expired - re-authentication needed'); + return await triggerReauth(agent); + } + + console.log('Session is valid'); + return agent; +} +``` + +```python Python +from kernel import Kernel + +kernel = Kernel() + +async def check_and_refresh_auth(agent_id: str): + agent = await kernel.agents.auth.retrieve(agent_id) + + print(f"Status: {agent.status}") + print(f"Last check: {agent.last_auth_check_at}") + + if agent.status == "NEEDS_AUTH": + print("Session expired - re-authentication needed") + return await trigger_reauth(agent) + + print("Session is valid") + return agent +``` + diff --git a/agent-auth/hosted-ui.mdx b/agent-auth/hosted-ui.mdx index de66c66..4167b2b 100644 --- a/agent-auth/hosted-ui.mdx +++ b/agent-auth/hosted-ui.mdx @@ -1,11 +1,9 @@ --- title: "Hosted UI" -description: "The simplest way to authenticate users - redirect to our hosted authentication UI" +description: "The simplest way to authenticate AI agents" --- -The Hosted UI flow is the recommended approach for most applications. You redirect users to a Kernel-hosted authentication page where they complete the login process, then poll for completion. - -## When to Use Hosted UI +Our Hosted UI flow is the recommended approach for most applications. Your application redirects users to a Kernel-hosted authentication page where they complete the login process. Then, you can poll for completion. Use the Hosted UI when: - Building user-facing applications where the user can complete login @@ -13,6 +11,112 @@ Use the Hosted UI when: - You need a quick integration with minimal code - You want to avoid handling multi-step auth flows yourself +## Getting started + +### 1. Create an Auth Agent + +An Auth Agent represents a (domain, [profile](/browsers/profiles)) pair. Creating one is idempotent—if an agent already exists for the domain and profile, it returns the existing one. + +```typescript +const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', // Required: domain to authenticate + profile_name: 'my-profile', // Required: name a profile to store the auth session + login_url: 'https://example.com/login', // Optional: speeds up discovery +}); +``` + +### 2. Start an Invocation + +An invocation starts a new authentication attempt. It returns a `hosted_url` where the user completes login. + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); +``` + +Enable automatic re-authentication when sessions expire with the `save_credential_as` field. When set, the Auth Agent automatically re-authenticates without additional user interaction (see [FAQ](/agent-auth/faq)). + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-saved-creds', // Set identifier to enable automatic re-auth +}); +``` + + +If the response returns `logged_in` as `true`, the profile is already authenticated. Skip the redirect and go straight to using the profile. + + +### 3. Redirect the User + +In a web application, redirect the user to the hosted URL: + +```typescript +// Frontend code +window.location.href = invocation.hosted_url; +``` + +The hosted UI will: +1. Navigate to the target domain +2. Find and display the login form +3. Let the user enter credentials +4. Handle multi-step auth (2FA, OTP, etc.) +5. Save the authenticated session to the profile + +### 4. Poll for Completion + +On your backend, poll the invocation status until it completes: + +```typescript +let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); + +while (status.status === 'IN_PROGRESS') { + await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds + status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); +} + +switch (status.status) { + case 'SUCCESS': + console.log('Authentication successful!'); + break; + case 'EXPIRED': + console.log('User did not complete login in time'); + break; + case 'CANCELED': + console.log('Authentication was canceled'); + break; +} +``` + + +Poll every 2 seconds with a maximum timeout of 5 minutes. The invocation expires after 5 minutes if not completed. + + +### 5. Use the Authenticated Profile + +Once authentication succeeds, create browsers with the profile to get an already logged-in session: + +```typescript +const browser = await kernel.browsers.create({ + profile: { name: 'my-profile' }, + stealth: true, +}); + +// Connect with Playwright +import { chromium } from 'playwright'; +const playwrightBrowser = await chromium.connectOverCDP(browser.cdp_ws_url); +const page = playwrightBrowser.contexts()[0].pages()[0]; + +// Navigate - you're already logged in! +await page.goto('https://example.com/dashboard'); +``` + + +Use `stealth: true` when creating browsers for authenticated sessions. Agent Auth runs authentication with stealth mode enabled, and mismatched settings may cause issues with bot detection. + + + ## Complete Example @@ -147,209 +251,6 @@ await kernel.browsers.delete_by_id(browser.session_id) ``` -## Step-by-Step Breakdown - -### 1. Create an Auth Agent - -An Auth Agent represents a (domain, profile) pair. Creating one is idempotent—if an agent already exists for the domain and profile, it returns the existing one. - -```typescript -const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', // Required: domain to authenticate - profile_name: 'my-profile', // Required: profile to store session - login_url: 'https://example.com/login', // Optional: speeds up discovery -}); -``` - -**Parameters:** -| Parameter | Required | Description | -|-----------|----------|-------------| -| `target_domain` | Yes | The domain to authenticate (e.g., `netflix.com`) | -| `profile_name` | Yes | Name of the profile to store the authenticated session | -| `login_url` | No | Direct URL to the login page. Providing this speeds up discovery. | -| `proxy` | No | Proxy configuration (see [Proxies](/proxies/overview)) | - -### 2. Start an Invocation - -An invocation starts a new authentication attempt. It returns a `hosted_url` where the user completes login. - -```typescript -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, -}); -``` - -**Response fields:** -| Field | Description | -|-------|-------------| -| `invocation_id` | Unique ID for this auth attempt | -| `hosted_url` | URL to redirect the user to | -| `handoff_code` | One-time code for programmatic flows | -| `expires_at` | When the invocation expires (5 minutes) | -| `logged_in` | If `true`, already authenticated—no login needed | - - -If `logged_in` is `true`, the profile is already authenticated. Skip the redirect and go straight to using the profile. - - -### 3. Redirect the User - -In a web application, redirect the user to the hosted URL: - -```typescript -// Frontend code -window.location.href = invocation.hosted_url; -``` - -The hosted UI will: -1. Navigate to the target domain -2. Find and display the login form -3. Let the user enter credentials -4. Handle multi-step auth (2FA, OTP, etc.) -5. Save the authenticated session to the profile - -### 4. Poll for Completion - -On your backend, poll the invocation status until it completes: - -```typescript -let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); - -while (status.status === 'IN_PROGRESS') { - await new Promise(r => setTimeout(r, 2000)); // Poll every 2 seconds - status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); -} - -switch (status.status) { - case 'SUCCESS': - console.log('Authentication successful!'); - break; - case 'EXPIRED': - console.log('User did not complete login in time'); - break; - case 'CANCELED': - console.log('Authentication was canceled'); - break; -} -``` - - -Poll every 2 seconds with a maximum timeout of 5 minutes. The invocation expires after 5 minutes if not completed. - - -### 5. Use the Authenticated Profile - -Once authentication succeeds, create browsers with the profile to get an already-logged-in session: - -```typescript -const browser = await kernel.browsers.create({ - profile: { name: 'my-profile' }, - stealth: true, -}); - -// Connect with Playwright -import { chromium } from 'playwright'; -const playwrightBrowser = await chromium.connectOverCDP(browser.cdp_ws_url); -const page = playwrightBrowser.contexts()[0].pages()[0]; - -// Navigate - you're already logged in! -await page.goto('https://example.com/dashboard'); -``` - - -Use `stealth: true` when creating browsers for authenticated sessions. Agent Auth runs authentication with stealth mode enabled, and mismatched settings may cause issues with bot detection. - - -## Using Proxies - -If the target site requires a specific IP or region, configure a proxy: - -```typescript -// Create auth agent with proxy -const agent = await kernel.agents.auth.create({ - target_domain: 'region-locked-site.com', - profile_name: 'my-profile', - proxy: { proxy_id: 'proxy_abc123' }, -}); - -// Use the same proxy when creating browsers -const browser = await kernel.browsers.create({ - profile: { name: 'my-profile' }, - proxy_id: 'proxy_abc123', - stealth: true, -}); -``` - - -Use the same proxy configuration for both the Auth Agent and subsequent browser sessions. Different IPs may trigger security measures on the target site. - - -## Saving Credentials for Re-auth - -You can save credentials during the hosted flow to enable automatic re-authentication when sessions expire: - -```typescript -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'my-saved-creds', // Save credentials for future re-auth -}); -``` - -When credentials are saved, the Auth Agent can automatically re-authenticate without user interaction. See [Session Monitoring](/agent-auth/session-monitoring) for details. - -## Handling Callback URLs - -For web applications, you may want to redirect users back to your app after authentication completes. Use the invocation's `hosted_url` as a starting point and implement webhook or polling patterns: - -```typescript -// Backend: Start auth flow -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, -}); - -// Frontend: Redirect to hosted UI -window.location.href = invocation.hosted_url; - -// Backend: Poll for completion and notify frontend via WebSocket or SSE -// OR: Use a webhook URL (coming soon) -``` - -## Error Handling - -Handle common error scenarios: - -```typescript -try { - const agent = await kernel.agents.auth.create({ - target_domain: 'example.com', - profile_name: 'my-profile', - }); - - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - }); - - // ... polling logic ... -} catch (error) { - if (error.status === 401) { - console.error('Invalid API key'); - } else if (error.status === 404) { - console.error('Auth agent not found'); - } else if (error.status === 409) { - console.error('Conflict - invocation already in progress'); - } else { - console.error('Unexpected error:', error.message); - } -} -``` - -## Next Steps +## Notes - - - Build custom auth flows with full control - - - Store credentials for automated re-auth - - +- For web applications, you may want to redirect users back to your app after authentication completes. Use the invocation's `hosted_url` as a starting point and implement polling patterns on your backend to check for when the Agent Auth completed. diff --git a/agent-auth/overview.mdx b/agent-auth/overview.mdx index cf66ef5..60d947d 100644 --- a/agent-auth/overview.mdx +++ b/agent-auth/overview.mdx @@ -1,9 +1,9 @@ --- title: "Overview" -description: "Secure authentication for AI agents - log users into any website and save authenticated browser sessions" +description: "Securely login AI agents to any website" --- -Agent Auth is an AI-powered authentication system that helps you log users into any website and save their authenticated browser session (profile) for future automations. It uses web agents to intelligently discover login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. +Agent Auth is an AI-powered platform that allows you to securely login to websites on behalf of end users and use their authentication (profile) in future automations. It intelligently discovers login forms, dynamically generate credential inputs, and handle multi-step authentication flows like 2FA/OTP. ## Why Agent Auth? @@ -16,61 +16,25 @@ Agent Auth solves this by providing: - **Multi-step auth handling** - Handles 2FA, OTP, magic links, and CAPTCHAs - **Credential security** - Credentials are encrypted and submitted programmatically—never stored in plaintext or shared with LLMs - **Session persistence** - Saves authenticated browser profiles for reuse -- **Automatic session monitoring** - Hourly checks detect when re-authentication is needed +- **Automatic session monitoring** - Regular checks detect when re-authentication is needed ## How It Works - Your backend calls `kernel.agents.auth.create()` to create an Auth Agent for a specific domain and profile combination. + Call `kernel.agents.auth.create()` to initialize an Auth Agent for a specific domain and [profile](/browsers/profiles) combination. - Call `kernel.agents.auth.invocations.create()` to start the auth flow. This returns a `hosted_url` where users complete login, or you can use the programmatic API for headless flows. + Call `kernel.agents.auth.invocations.create()` to start the auth flow. This returns a `hosted_url` where the end user can complete their login. You can also build your own user experience a using our headless API. - The user is redirected to the hosted UI where they enter credentials and handle 2FA if needed. The profile is saved on success. + The end user is redirected to the hosted UI where they enter credentials and handle 2FA if needed. The profile is saved on success. - Create browsers with the saved profile using `kernel.browsers.create({ profile: { name: 'my-profile' } })`. The browser is already logged in. + Create browsers with the saved profile using `kernel.browsers.create({ profile: { name: 'my-profile' } })`. Browsers start already logged in. -## Key Concepts - -### Auth Agent - -An **Auth Agent** is a persistent entity that manages authentication for a specific domain + profile combination. It stores: - -- The target domain (e.g., `netflix.com`) -- The linked browser profile -- The login URL (learned during discovery) -- Current authentication status (`AUTHENTICATED` or `NEEDS_AUTH`) -- Optional linked credentials for automated re-auth - -Auth Agents are **idempotent**—if an agent already exists for the domain and profile, `kernel.agents.auth.create()` returns the existing one. - -### Invocation - -An **Invocation** is a single authentication attempt. You start one by calling `kernel.agents.auth.invocations.create()`. Over time, an Auth Agent accumulates multiple invocations as sessions need to be established or refreshed. - -**Invocation statuses:** -- `IN_PROGRESS` - User is completing the auth flow -- `SUCCESS` - Authentication completed, profile saved -- `EXPIRED` - Invocation timed out (5 minutes) -- `CANCELED` - Invocation was explicitly canceled - -### Profile - -A **Profile** stores browser session state (cookies, localStorage) that persists across browser sessions. Once authenticated via Agent Auth, use the profile with `kernel.browsers.create()` to get an already-logged-in browser. - -See [Profiles](/browsers/profiles) for more details on working with profiles. - -### Credentials - -**Credentials** are securely stored login credentials that enable automated re-authentication when sessions expire. Values are encrypted at rest and never exposed to LLMs or API responses. - -See [Credentials](/agent-auth/credentials) for details on storing and using credentials. - ## Integration Options Agent Auth offers two integration approaches: @@ -105,106 +69,3 @@ Agent Auth is designed with security as a first principle: | **One-time handoff codes** | Handoff codes can only be exchanged once and expire after 5 minutes | | **Profile encryption** | Browser profiles are encrypted end-to-end | | **Isolated execution** | Each auth flow runs in an isolated browser environment | - -## Quick Example - -Here's a complete example using the Hosted UI flow: - - -```typescript TypeScript -import Kernel from '@onkernel/sdk'; - -const kernel = new Kernel(); - -// 1. Create or get existing Auth Agent -const agent = await kernel.agents.auth.create({ - target_domain: 'netflix.com', - profile_name: 'netflix-user-123', -}); - -// 2. Start the auth flow -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, -}); - -// 3. Check if already logged in -if (invocation.status === 'ALREADY_AUTHENTICATED') { - console.log('Already authenticated!'); -} else { - // Redirect user to hosted UI - console.log('Redirect to:', invocation.hosted_url); -} - -// 4. Poll for completion -let status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); -while (status.status === 'IN_PROGRESS') { - await new Promise(r => setTimeout(r, 2000)); - status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id); -} - -if (status.status === 'SUCCESS') { - // 5. Use the authenticated profile - const browser = await kernel.browsers.create({ - profile: { name: 'netflix-user-123' }, - stealth: true, - }); - console.log('Browser ready:', browser.cdp_ws_url); -} -``` - -```python Python -from kernel import Kernel -import asyncio - -kernel = Kernel() - -# 1. Create or get existing Auth Agent -agent = await kernel.agents.auth.create( - target_domain="netflix.com", - profile_name="netflix-user-123", -) - -# 2. Start the auth flow -invocation = await kernel.agents.auth.invocations.create( - auth_agent_id=agent.id, -) - -# 3. Check if already logged in -if invocation.status == "ALREADY_AUTHENTICATED": - print("Already authenticated!") -else: - # Redirect user to hosted UI - print(f"Redirect to: {invocation.hosted_url}") - -# 4. Poll for completion -status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) -while status.status == "IN_PROGRESS": - await asyncio.sleep(2) - status = await kernel.agents.auth.invocations.retrieve(invocation.invocation_id) - -if status.status == "SUCCESS": - # 5. Use the authenticated profile - browser = await kernel.browsers.create( - profile={"name": "netflix-user-123"}, - stealth=True, - ) - print(f"Browser ready: {browser.cdp_ws_url}") -``` - - -## Next Steps - - - - Get started with the simplest integration path - - - Build custom auth flows with full control - - - Store credentials for automated re-auth - - - Keep sessions alive automatically - - diff --git a/agent-auth/programmatic.mdx b/agent-auth/programmatic.mdx index e44c3bd..1c684bd 100644 --- a/agent-auth/programmatic.mdx +++ b/agent-auth/programmatic.mdx @@ -3,9 +3,7 @@ title: "Programmatic Flow" description: "Build custom authentication UIs with full control using discover and submit APIs" --- -The Programmatic flow gives you complete control over the authentication process. Instead of redirecting users to the hosted UI, you build your own UI and use the discover/submit APIs to drive the authentication. - -## When to Use Programmatic Flow +Our Programmatic flow gives you complete control over the user experience to generate Agent. Instead of redirecting users to the hosted UI, you build your own UI and use the discover/submit APIs to drive the authentication. Use the Programmatic flow when: - You need a custom authentication UI that matches your app's design @@ -17,7 +15,7 @@ Use the Programmatic flow when: - Same as Hosted UI - create an agent and start an invocation + Create an auth agent and start an invocation (same as [Hosted UI](/agent-auth/hosted-ui)) Exchange the one-time handoff code for a scoped JWT token @@ -33,6 +31,145 @@ Use the Programmatic flow when: +## Getting started + +### 1. Create an Auth Agent + +An Auth Agent represents a (domain, [profile](/browsers/profiles)) pair. Creating one is idempotent—if an agent already exists for the domain and profile, it returns the existing one. + +```typescript +const agent = await kernel.agents.auth.create({ + target_domain: 'example.com', // Required: domain to authenticate + profile_name: 'my-profile', // Required: name a profile to store the auth session + login_url: 'https://example.com/login', // Optional: speeds up discovery +}); +``` + +### 2. Start an Invocation + +An invocation starts a new authentication attempt. It returns a `handoff_code` to create a JWT. + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, +}); +``` + +Enable automatic re-authentication when sessions expire with the `save_credential_as` field. When set, the Auth Agent automatically re-authenticates without additional user interaction (see [FAQ](/agent-auth/faq)). + +```typescript +const invocation = await kernel.agents.auth.invocations.create({ + auth_agent_id: agent.id, + save_credential_as: 'my-saved-creds', // Set identifier to enable automatic re-auth +}); +``` + + +If the response returns `logged_in` as `true`, the profile is already authenticated. Skip the redirect and go straight to using the profile. + + +### 3. Exchange Handoff Code for JWT + +The handoff code is a one-time credential that you exchange for a JWT token. This JWT is scoped to the specific invocation and grants permission to control the browser session. + +```typescript +const exchange = await kernel.agents.auth.invocations.exchange( + invocation.invocation_id, + { code: invocation.handoff_code } +); + +const jwt = exchange.jwt; // Valid for 30 minutes +``` + + +The handoff code can only be used once and expires after 5 minutes. The JWT is valid for 30 minutes. + + +### 4. Create JWT-Authenticated Client + +Create a new Kernel client using the JWT for subsequent API calls: + +```typescript +const jwtKernel = new Kernel({ apiKey: exchange.jwt }); +``` + +This client has limited permissions—it can only call the discover and submit endpoints for this specific invocation. + +### 5. Discover Login Fields + +Call `discover()` to navigate to the login page and extract form fields: + +```typescript +const discoverResponse = await jwtKernel.agents.auth.invocations.discover( + invocation.invocation_id, + { login_url: 'https://example.com/login' } // Optional: override login URL +); +``` + + +If `discover()` returns `logged_in: true`, the profile is already authenticated from a previous session. No credentials are needed—proceed to using the profile. + + +### 6. Map Credentials to Fields + +The discover response returns an array of fields with `name`, `type`, and `label` properties. Use the `name` as the key when submitting credentials: + +```typescript +// Discovered fields +const fields = [ + { name: 'email', type: 'email', label: 'Email Address' }, + { name: 'password', type: 'password', label: 'Password' } +]; + +// Your stored credentials +const credentials = { + email: 'user@example.com', + password: 'secretpassword' +}; + +// Map to field_values using field.name as key +const fieldValues = { + 'email': credentials.email, // field.name → credential value + 'password': credentials.password +}; +``` + +### 7. Submit Credentials + +Submit the mapped field values to attempt login: + +```typescript +const submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: fieldValues } +); +``` + +### 8. Handle Multi-Step Auth + +When `needs_additional_auth` is `true`, this means the webpage is showing new fields required for submission (typically 2FA/OTP). Collect the additional values and submit again: + +```typescript +while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { + // Show the additional fields to the user + console.log('Additional fields needed:', submitResponse.additional_fields); + + // Collect values (e.g., prompt user for 2FA code) + const additionalValues: Record = {}; + for (const field of submitResponse.additional_fields) { + if (field.type === 'code' || field.name.includes('code')) { + additionalValues[field.name] = await promptUserForOTP(); + } + } + + // Submit again + submitResponse = await jwtKernel.agents.auth.invocations.submit( + invocation.invocation_id, + { field_values: additionalValues } + ); +} +``` + ## Complete Example @@ -233,287 +370,9 @@ def map_credentials_to_fields(fields, credentials): ``` -## Step-by-Step Breakdown - -### 1. Exchange Handoff Code for JWT - -The handoff code is a one-time credential that you exchange for a JWT token. This JWT is scoped to the specific invocation and grants permission to control the browser session. - -```typescript -const exchange = await kernel.agents.auth.invocations.exchange( - invocation.invocation_id, - { code: invocation.handoff_code } -); - -const jwt = exchange.jwt; // Valid for 30 minutes -``` - - -The handoff code can only be used once and expires after 5 minutes. The JWT is valid for 30 minutes. - - -### 2. Create JWT-Authenticated Client - -Create a new Kernel client using the JWT for subsequent API calls: - -```typescript -const jwtKernel = new Kernel({ apiKey: exchange.jwt }); -``` - -This client has limited permissions—it can only call the discover and submit endpoints for this specific invocation. - -### 3. Discover Login Fields - -Call `discover()` to navigate to the login page and extract form fields: - -```typescript -const discoverResponse = await jwtKernel.agents.auth.invocations.discover( - invocation.invocation_id, - { login_url: 'https://example.com/login' } // Optional: override login URL -); -``` - -**Discover response:** - -```json -{ - "success": true, - "logged_in": false, - "login_url": "https://identity.example.com/login", - "page_title": "Sign In - Example", - "fields": [ - { - "name": "email", - "type": "email", - "label": "Email Address", - "placeholder": "Enter your email", - "required": true, - "selector": "//input[@id='email']" - }, - { - "name": "password", - "type": "password", - "label": "Password", - "required": true, - "selector": "//input[@id='password']" - } - ] -} -``` - - -If `discover()` returns `logged_in: true`, the profile is already authenticated from a previous session. No credentials are needed—proceed to using the profile. - - -### 4. Map Credentials to Fields - -The discover response returns an array of fields with `name`, `type`, and `label` properties. Use the `name` as the key when submitting credentials: - -```typescript -// Discovered fields -const fields = [ - { name: 'email', type: 'email', label: 'Email Address' }, - { name: 'password', type: 'password', label: 'Password' } -]; - -// Your stored credentials -const credentials = { - email: 'user@example.com', - password: 'secretpassword' -}; - -// Map to field_values using field.name as key -const fieldValues = { - 'email': credentials.email, // field.name → credential value - 'password': credentials.password -}; -``` - -**Field type reference:** - -| Type | Description | Example Values | -|------|-------------|----------------| -| `text` | Generic text input | Username, name | -| `email` | Email address | user@example.com | -| `password` | Password (masked) | ••••••••• | -| `tel` | Phone number | +1-555-0123 | -| `number` | Numeric input | 12345 | -| `code` | Verification code (OTP/2FA) | 123456 | - -### 5. Submit Credentials - -Submit the mapped field values to attempt login: - -```typescript -const submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: fieldValues } -); -``` - -**Possible submit responses:** - -**Success:** -```json -{ - "success": true, - "logged_in": true, - "app_name": "My App", - "target_domain": "example.com" -} -``` - -**Needs additional auth (2FA):** -```json -{ - "success": true, - "logged_in": false, - "needs_additional_auth": true, - "additional_fields": [ - { - "name": "code", - "type": "code", - "label": "Verification Code", - "required": true, - "selector": "//input[@name='code']" - } - ] -} -``` - -**Error:** -```json -{ - "success": false, - "logged_in": false, - "error_message": "Incorrect email or password" -} -``` - -### 6. Handle Multi-Step Auth - -When `needs_additional_auth` is `true`, the page is showing new fields (typically 2FA/OTP). Collect the additional values and submit again: - -```typescript -while (submitResponse.needs_additional_auth && submitResponse.additional_fields) { - // Show the additional fields to the user - console.log('Additional fields needed:', submitResponse.additional_fields); - - // Collect values (e.g., prompt user for 2FA code) - const additionalValues: Record = {}; - for (const field of submitResponse.additional_fields) { - if (field.type === 'code' || field.name.includes('code')) { - additionalValues[field.name] = await promptUserForOTP(); - } - } - - // Submit again - submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: additionalValues } - ); -} -``` - -## Building a Custom Login UI - -Here's an example of rendering discovered fields in a React component: - -```tsx -function LoginForm({ fields, onSubmit }) { - const [values, setValues] = useState>({}); - - return ( -
{ - e.preventDefault(); - onSubmit(values); - }}> - {fields.map((field) => ( -
- - setValues({ - ...values, - [field.name]: e.target.value - })} - /> -
- ))} - -
- ); -} -``` - -## Saving Credentials During Programmatic Flow - -You can save credentials during the invocation for future automated re-auth: - -```typescript -const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agent.id, - save_credential_as: 'my-login-creds', // Credentials will be saved -}); - -// ... complete the programmatic flow ... -// Credentials are automatically saved when login succeeds -``` - -See [Credentials](/agent-auth/credentials) for details on pre-storing credentials. - -## Error Handling - -```typescript -try { - const discoverResponse = await jwtKernel.agents.auth.invocations.discover( - invocation.invocation_id, - {} - ); - - if (!discoverResponse.success) { - console.error('Discovery failed:', discoverResponse.error_message); - return; - } - - const submitResponse = await jwtKernel.agents.auth.invocations.submit( - invocation.invocation_id, - { field_values: fieldValues } - ); - - if (!submitResponse.success) { - console.error('Login failed:', submitResponse.error_message); - // Show error to user, let them retry - } -} catch (error) { - if (error.status === 401) { - console.error('JWT expired - exchange a new handoff code'); - } else if (error.status === 404) { - console.error('Invocation not found or expired'); - } else { - console.error('Unexpected error:', error.message); - } -} -``` - -## Security Considerations +## Notes - The JWT is scoped to the specific invocation and cannot access other resources - Credentials submitted via `submit()` are sent directly to the target site - Credentials are never logged, stored in plaintext, or passed to LLMs - The browser session is isolated and destroyed after the invocation completes - -## Next Steps - - - - Pre-store credentials for fully automated auth - - - Keep sessions alive automatically - - diff --git a/agent-auth/session-monitoring.mdx b/agent-auth/session-monitoring.mdx deleted file mode 100644 index 009e93b..0000000 --- a/agent-auth/session-monitoring.mdx +++ /dev/null @@ -1,402 +0,0 @@ ---- -title: "Session Monitoring" -description: "Automatic session health checks and re-authentication" ---- - -Agent Auth automatically monitors authenticated sessions to detect when they expire. This enables proactive re-authentication before your workflows fail. - -## How Session Monitoring Works - -Auth Agents with `AUTHENTICATED` status are automatically checked hourly to verify the session is still valid. The check: - -1. Opens a browser with the authenticated profile -2. Navigates to the auth check URL (usually the main site) -3. Determines if the user is still logged in -4. Updates the Auth Agent status if the session has expired - -``` -┌─────────────────────┐ -│ Auth Agent │ -│ status: AUTH │ -└─────────┬───────────┘ - │ hourly check - ▼ -┌─────────────────────┐ -│ Check Session │ -│ - Load profile │ -│ - Navigate to site│ -│ - Detect login │ -└─────────┬───────────┘ - │ - ┌─────┴─────┐ - ▼ ▼ - Still Session - logged expired - in - │ │ - ▼ ▼ - No change Update status - → NEEDS_AUTH -``` - -## Detecting Expired Sessions - -Check the Auth Agent status to determine if re-authentication is needed: - - -```typescript TypeScript -import Kernel from '@onkernel/sdk'; - -const kernel = new Kernel(); - -async function checkAndRefreshAuth(agentId: string) { - const agent = await kernel.agents.auth.retrieve(agentId); - - console.log('Status:', agent.status); - console.log('Last check:', agent.last_auth_check_at); - - if (agent.status === 'NEEDS_AUTH') { - console.log('Session expired - re-authentication needed'); - return await triggerReauth(agent); - } - - console.log('Session is valid'); - return agent; -} -``` - -```python Python -from kernel import Kernel - -kernel = Kernel() - -async def check_and_refresh_auth(agent_id: str): - agent = await kernel.agents.auth.retrieve(agent_id) - - print(f"Status: {agent.status}") - print(f"Last check: {agent.last_auth_check_at}") - - if agent.status == "NEEDS_AUTH": - print("Session expired - re-authentication needed") - return await trigger_reauth(agent) - - print("Session is valid") - return agent -``` - - -**Auth Agent status values:** - -| Status | Description | -|--------|-------------| -| `AUTHENTICATED` | Session is valid and ready to use | -| `NEEDS_AUTH` | Session has expired, re-authentication required | - -## Triggering Re-authentication - -When a session expires, you have two options: - -### Option 1: Automated Re-auth (Requires Credentials) - -If the Auth Agent has linked credentials and saved selectors, trigger automated re-auth: - -```typescript -async function triggerReauth(agent) { - if (!agent.can_reauth) { - console.log('Cannot auto-reauth - manual login required'); - // Fall back to hosted UI flow - return await manualReauth(agent.id); - } - - const reauth = await kernel.agents.auth.reauth(agent.id); - - switch (reauth.status) { - case 'REAUTH_STARTED': - console.log('Re-auth started:', reauth.invocation_id); - // Poll for completion - return await pollForCompletion(reauth.invocation_id); - - case 'ALREADY_AUTHENTICATED': - console.log('Already authenticated'); - return { success: true }; - - case 'CANNOT_REAUTH': - console.log('Cannot reauth:', reauth.message); - return await manualReauth(agent.id); - } -} -``` - -### Option 2: Manual Re-auth (Hosted UI) - -If credentials aren't stored, redirect the user to complete login: - -```typescript -async function manualReauth(agentId: string) { - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: agentId, - save_credential_as: 'my-saved-creds', // Save for future auto-reauth - }); - - console.log('Redirect user to:', invocation.hosted_url); - // User completes login... - - return await pollForCompletion(invocation.invocation_id); -} -``` - -## Polling for Re-auth Completion - -After triggering re-auth, poll for completion: - -```typescript -async function pollForCompletion(invocationId: string) { - const maxWaitMs = 5 * 60 * 1000; // 5 minutes - const startTime = Date.now(); - - while (Date.now() - startTime < maxWaitMs) { - const status = await kernel.agents.auth.invocations.retrieve(invocationId); - - if (status.status === 'SUCCESS') { - console.log('Re-authentication successful!'); - return { success: true }; - } - - if (status.status === 'EXPIRED' || status.status === 'CANCELED') { - console.log('Re-auth failed:', status.status); - return { success: false, reason: status.status }; - } - - await new Promise(r => setTimeout(r, 2000)); - } - - return { success: false, reason: 'TIMEOUT' }; -} -``` - -## Proactive Session Management - -Instead of waiting for automations to fail, proactively check and refresh sessions: - -```typescript -async function ensureAuthenticated(agentId: string) { - const agent = await kernel.agents.auth.retrieve(agentId); - - // Already authenticated - if (agent.status === 'AUTHENTICATED') { - return agent; - } - - // Need to re-auth - if (agent.can_reauth) { - const reauth = await kernel.agents.auth.reauth(agentId); - if (reauth.status === 'REAUTH_STARTED') { - await pollForCompletion(reauth.invocation_id); - } - } else { - throw new Error('Session expired and cannot auto-reauth'); - } - - // Return refreshed agent - return await kernel.agents.auth.retrieve(agentId); -} - -// Use before running automations -async function runAutomation(agentId: string, profileName: string) { - // Ensure authenticated before starting - await ensureAuthenticated(agentId); - - // Create browser with authenticated profile - const browser = await kernel.browsers.create({ - profile: { name: profileName }, - stealth: true, - }); - - // Run your automation... -} -``` - -## Monitoring Multiple Auth Agents - -For applications with many authenticated sessions, batch-check auth status: - -```typescript -async function checkAllSessions() { - const agents = await kernel.agents.auth.list(); - const needsReauth = []; - const canAutoReauth = []; - const needsManualReauth = []; - - for (const agent of agents.items) { - if (agent.status === 'NEEDS_AUTH') { - needsReauth.push(agent); - if (agent.can_reauth) { - canAutoReauth.push(agent); - } else { - needsManualReauth.push(agent); - } - } - } - - console.log(`Total agents: ${agents.items.length}`); - console.log(`Needs re-auth: ${needsReauth.length}`); - console.log(`Can auto-reauth: ${canAutoReauth.length}`); - console.log(`Needs manual login: ${needsManualReauth.length}`); - - // Auto-reauth those that can be automated - for (const agent of canAutoReauth) { - await kernel.agents.auth.reauth(agent.id); - } - - return { needsManualReauth }; -} -``` - -## Complete Example: Robust Session Management - -Here's a complete pattern for robust session handling: - -```typescript -import Kernel from '@onkernel/sdk'; - -const kernel = new Kernel(); - -class AuthSessionManager { - async getAuthenticatedBrowser( - agentId: string, - profileName: string, - options?: { forceRefresh?: boolean } - ) { - // Check current status - let agent = await kernel.agents.auth.retrieve(agentId); - - // Force refresh if requested - if (options?.forceRefresh && agent.can_reauth) { - await this.triggerReauth(agent); - agent = await kernel.agents.auth.retrieve(agentId); - } - - // Handle expired sessions - if (agent.status === 'NEEDS_AUTH') { - if (agent.can_reauth) { - await this.triggerReauth(agent); - agent = await kernel.agents.auth.retrieve(agentId); - } else { - throw new AuthRequiredError( - 'Session expired - manual login required', - agentId - ); - } - } - - // Verify authentication succeeded - if (agent.status !== 'AUTHENTICATED') { - throw new AuthFailedError('Authentication failed', agentId); - } - - // Create and return authenticated browser - return await kernel.browsers.create({ - profile: { name: profileName }, - stealth: true, - }); - } - - private async triggerReauth(agent: AuthAgent) { - const reauth = await kernel.agents.auth.reauth(agent.id); - - if (reauth.status === 'REAUTH_STARTED' && reauth.invocation_id) { - await this.pollForCompletion(reauth.invocation_id); - } else if (reauth.status === 'CANNOT_REAUTH') { - throw new CannotReauthError(reauth.message, agent.id); - } - } - - private async pollForCompletion(invocationId: string, timeoutMs = 300000) { - const startTime = Date.now(); - - while (Date.now() - startTime < timeoutMs) { - const status = await kernel.agents.auth.invocations.retrieve(invocationId); - - if (status.status === 'SUCCESS') return; - if (status.status === 'EXPIRED') throw new Error('Re-auth expired'); - if (status.status === 'CANCELED') throw new Error('Re-auth canceled'); - - await new Promise(r => setTimeout(r, 2000)); - } - - throw new Error('Re-auth timeout'); - } -} - -// Custom error types -class AuthRequiredError extends Error { - constructor(message: string, public agentId: string) { - super(message); - this.name = 'AuthRequiredError'; - } -} - -class AuthFailedError extends Error { - constructor(message: string, public agentId: string) { - super(message); - this.name = 'AuthFailedError'; - } -} - -class CannotReauthError extends Error { - constructor(message: string, public agentId: string) { - super(message); - this.name = 'CannotReauthError'; - } -} - -// Usage -const sessionManager = new AuthSessionManager(); - -try { - const browser = await sessionManager.getAuthenticatedBrowser( - 'agent_abc123', - 'my-profile' - ); - // Use the browser... -} catch (error) { - if (error instanceof AuthRequiredError) { - // Redirect user to complete login - const invocation = await kernel.agents.auth.invocations.create({ - auth_agent_id: error.agentId, - }); - console.log('Login required:', invocation.hosted_url); - } else { - throw error; - } -} -``` - -## Best Practices - -1. **Check before automation** - Always verify auth status before starting long-running automations - -2. **Enable auto-reauth** - Store credentials to enable automated re-authentication - -3. **Handle failures gracefully** - Have a fallback to manual login when auto-reauth fails - -4. **Monitor auth status** - Periodically check auth agents to proactively handle expirations - -5. **Log auth events** - Track when re-auth happens for debugging and auditing - -## Session Check Frequency - -- Auth checks run approximately **every hour** for authenticated agents -- Checks are passive and don't modify the saved profile -- The `last_auth_check_at` field shows when the last check occurred - -## Next Steps - - - - Store credentials to enable auto-reauth - - - Review Agent Auth concepts - - diff --git a/docs.json b/docs.json index 3406726..d175ccb 100644 --- a/docs.json +++ b/docs.json @@ -62,17 +62,6 @@ "quickstart" ] }, - { - "group": "Agent Auth", - "pages": [ - "agent-auth/overview", - "agent-auth/hosted-ui", - "agent-auth/programmatic", - "agent-auth/credentials", - "agent-auth/session-monitoring", - "agent-auth/early-preview" - ] - }, { "group": "Browsers", "pages": [ @@ -109,6 +98,16 @@ "browsers/faq" ] }, + { + "group": "Agent Auth", + "pages": [ + "agent-auth/overview", + "agent-auth/hosted-ui", + "agent-auth/programmatic", + "agent-auth/credentials", + "agent-auth/faq" + ] + }, { "group": "App Platform", "pages": [