web-auth-bridge
Skill0 starsChanged 8 months agoMITView on GitHub
Use when building CLI tools or agent skills that need to authenticate against external websites protected by WAF, SAML, MFA, or complex browser-based login flows. Provides browser-based authentication via Playwright, token caching, httpx client injection, and parallel browser context pooling.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- adr-generationSkill
- bash-scriptingSkill
- browser-controlSkill
- github-actionsSkill
- gitignore-standardsSkill
- git-operationsSkill
- html-reportSkill
- java-developmentSkill
- mermaid-diagramsSkill
- meta-workspaceSkill
- powershell-scriptingSkill
- project-backlogSkill
- project-documentationSkill
- project-setupSkill
- python-developmentSkill
- refinement-tribunalSkill
- skill-cli-developmentSkill
- technical-writingSkill
- testing-standardsSkill
- typescript-developmentSkill
- worktree-isolationSkill
- writing-ai-instructionsSkill
The file
We split this skill into the rules it gives an agent
8 rules. About 0 are common, 4 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 90
Every target website needs its own AuthCallback.
- Distinctive: line 194
When credentials cannot be stored or the flow requires visual interaction:
- Distinctive: line 207
When building a skill CLI that uses web-auth-bridge, follow this pattern:
- Very specific: line 309
Use cache_dir under the consuming skill's config directory
- Distinctive: line 312
Follow skill-cli-development credential isolation rules
- Distinctive: line 318
Use web-auth-bridge for sites with simple API key authentication
- Very specific: line 323
Use StealthConfig to customize anti-detection settings per site
- Very specific: line 326
Use force_authenticate() to bypass cache during debugging
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Web Auth Bridge
Objective
Enable agent skills and CLI tools to authenticate against external websites that require browser-based login flows (Cloudflare WAF, SAML, MFA, ASP.NET sessions) and then interact with their APIs using cached tokens — without re-authenticating on every execution.
Scope
In-scope:
- Implementing
AuthCallbackclasses that drive site-specific login flows - Configuring
WebAuthBridgefor headless or headed authentication - Using cached authentication results with
httpxclients - Spinning up parallel browser contexts for sites requiring continued browser interaction
- Implementing
SessionRenewalCallbackfor stateful session management - Integrating web-auth-bridge into skill CLIs
Out-of-scope:
- General HTTP client usage without browser authentication
- Playwright usage unrelated to authentication
- Credential file creation (operator responsibility per skill-cli-development)
Library Location
The library is published at https://github.com/mikejhill/web-auth-bridge and is installed via:
uv add "web-auth-bridge @ git+https://github.com/mikejhill/web-auth-bridge"Or from a skill CLI's pyproject.toml:
[project]
dependencies = ["web-auth-bridge"]
[tool.uv.sources]
web-auth-bridge = { git = "https://github.com/mikejhill/web-auth-bridge" }For local development against a checkout, use uv add --editable <path-to-web-auth-bridge>.
Core Concepts
Architecture
┌─────────────────────────────────────────────────────┐
│ WebAuthBridge │
│ (facade: ties auth, cache, HTTP client, pool) │
├──────────────┬──────────────┬───────────────────────┤
│ Authenticator│ AuthCache │ BrowserContextPool │
│ (browser + │ (file-backed │ (N parallel contexts │
│ callback) │ persistence)│ with cloned cookies) │
├──────────────┴──────────────┴───────────────────────┤
│ BrowserManager (Playwright) │
└─────────────────────────────────────────────────────┘Flow
- Auth stage: Playwright browser runs consumer's
AuthCallback→ cookies/tokens extracted - Cache stage:
AuthResultpersisted to disk (file permissions600) - API stage (choose one):
- HTTP only:
httpx.AsyncClientorhttpx.Clientwith cookies pre-injected - Browser pool: N parallel
BrowserContextinstances with cookies cloned in
- HTTP only:
Key Classes
| Class | Import | Purpose |
WebAuthBridge | web_auth_bridge | Main facade — authenticate, get clients, manage pool |
AuthResult | web_auth_bridge | Cookies, tokens, localStorage from auth |
CookieData | web_auth_bridge | Single cookie with Playwright dict conversion |
Credentials | web_auth_bridge | Username/password/extra for headless auth |
AuthCallback | web_auth_bridge | Protocol: consumer implements authenticate() |
SessionRenewalCallback | web_auth_bridge | Protocol: per-context session renewal |
StealthConfig | web_auth_bridge | Anti-detection settings (user-agent, viewport) |
Implementation Guide
Step 1: Implement AuthCallback
Every target website needs its own AuthCallback. This is where the site-specific login logic lives.
from web_auth_bridge import AuthCallback, AuthResult, CookieData, Credentials
from playwright.async_api import Page
class MyServiceAuth:
"""Drive login for my-service.example.com."""
async def authenticate(self, page: Page, credentials: Credentials | None) -> AuthResult:
await page.goto("https://my-service.example.com/login")
if credentials:
await page.fill("#username", credentials.username)
await page.fill("#password", credentials.password)
await page.click("#submit")
else:
# Manual entry: wait for user to complete login
await page.wait_for_url("**/dashboard**", timeout=120_000)
await page.wait_for_load_state("networkidle")
# Extract cookies from the browser context
cookies = await page.context.cookies()
return AuthResult(
cookies=[CookieData.from_playwright_dict(dict(c)) for c in cookies],
tokens={"csrf": await page.evaluate("() => document.querySelector('meta[name=csrf]')?.content || ''")},
)
async def is_authenticated(self, auth_result: AuthResult) -> bool:
return not auth_result.is_expiredStep 2: Configure and Use WebAuthBridge
import asyncio
from pathlib import Path
from web_auth_bridge import WebAuthBridge, Credentials
async def main() -> None:
bridge = WebAuthBridge(
auth_callback=MyServiceAuth(),
cache_dir=Path("~/.config/agent-skills/my-skill/cache"),
credentials=Credentials(username="user@example.com", password="secret"),
headless=True,
)
# Authenticate (uses cache if valid)
result = await bridge.ensure_authenticated()
# Option A: HTTP API calls (no browser needed)
async with bridge.http_client() as client:
resp = await client.get("https://my-service.example.com/api/data")
print(resp.json())
# Option B: Sync HTTP client
with bridge.http_client_sync() as client:
resp = client.get("https://my-service.example.com/api/data")
print(resp.json())
# Option C: Parallel browser contexts (for sites requiring browser)
async with bridge.browser_pool(count=4) as contexts:
pages = [await ctx.new_page() for ctx in contexts]
for page in pages:
await page.goto("https://my-service.example.com/reports")
# ... scrape or interact
asyncio.run(main())Step 3: Handle Stateful Sessions (Optional)
For sites with stateful server-side sessions (ASP.NET, etc.), implement SessionRenewalCallback:
from web_auth_bridge import SessionRenewalCallback
from playwright.async_api import BrowserContext, Page
class AspNetSessionRenewal:
async def renew(
self,
context: BrowserContext,
page: Page,
auth_cookies: list[dict[str, object]],
) -> None:
await page.goto("https://portal.example.com/session/init")
await page.wait_for_load_state("networkidle")
# Server sets a unique ASP.NET_SessionId cookie automatically
# Use it:
async with bridge.browser_pool(count=4, session_renewal=AspNetSessionRenewal()) as contexts:
...Step 4: Headed Mode for Manual Auth
When credentials cannot be stored or the flow requires visual interaction:
bridge = WebAuthBridge(
auth_callback=MyServiceAuth(),
cache_dir=Path("~/.config/agent-skills/my-skill/cache"),
credentials=None, # No stored credentials
headless=False, # User sees the browser
)Integration with Skill CLIs
When building a skill CLI that uses web-auth-bridge, follow this pattern:
"""CLI for my-skill that authenticates via web-auth-bridge."""
import argparse
import asyncio
import sys
from pathlib import Path
from web_auth_bridge import WebAuthBridge, Credentials
def _load_credentials(config_dir: Path) -> Credentials | None:
creds_file = config_dir / "credentials.toml"
if not creds_file.exists():
return None
import tomllib
data = tomllib.loads(creds_file.read_text())
return Credentials(
username=data["username"],
password=data["password"],
)
async def _run(args: argparse.Namespace) -> None:
config_dir = Path("~/.config/agent-skills/my-skill").expanduser()
credentials = _load_credentials(config_dir)
bridge = WebAuthBridge(
auth_callback=MyServiceAuth(),
cache_dir=config_dir / "cache",
credentials=credentials,
headless=credentials is not None, # Headed if no stored creds
)
await bridge.ensure_authenticated()
async with bridge.http_client() as client:
resp = await client.get("https://api.example.com/data")
print(resp.text)
def main() -> None:
parser = argparse.ArgumentParser(description="My skill CLI")
args = parser.parse_args()
asyncio.run(_run(args))API Reference
WebAuthBridge
WebAuthBridge(
*,
auth_callback: AuthCallback,
cache_dir: Path,
credentials: Credentials | None = None,
headless: bool = True,
browser_type: str = "chromium",
stealth: StealthConfig | None = None,
launch_kwargs: dict[str, Any] | None = None,
)Methods:
| Method | Returns | Description |
await ensure_authenticated() | AuthResult | Auth if needed, return valid result |
await force_authenticate() | AuthResult | Force fresh auth, ignore cache |
http_client(**kwargs) | httpx.AsyncClient | Async HTTP client with auth cookies |
http_client_sync(**kwargs) | httpx.Client | Sync HTTP client with auth cookies |
cookies() | list[dict] | Raw cookies as Playwright dicts |
tokens() | dict[str, str] | Token key-value pairs |
browser_pool(count, *, session_renewal) | AsyncContextManager[list[BrowserContext]] | Parallel browser contexts |
invalidate_cache() | None | Delete cache, force re-auth next time |
AuthResult
| Field | Type | Description |
cookies | list[CookieData] | Extracted browser cookies |
local_storage | dict[str, str] | localStorage entries |
tokens | dict[str, str] | Arbitrary token key-value pairs |
expires_at | datetime | None | When the auth expires |
is_expired | bool (property) | Whether auth has expired |
Credentials
| Field | Type | Description |
username | str | Login username/email |
password | str | Login password |
extra | dict[str, str] | Additional fields (MFA secrets, etc.) |
Constraints
MUST:
- Implement
AuthCallbackfor each target website - Use
cache_dirunder the consuming skill's config directory - Set
headless=Falsewhen credentials are not stored (manual entry) - Handle
AuthErrorexceptions fromensure_authenticated() - Follow skill-cli-development credential isolation rules
MUST NOT:
- Store credentials in the auth cache (only tokens/cookies are cached)
- Share browser contexts across asyncio tasks (Playwright limitation)
- Use web-auth-bridge for sites with simple API key authentication
- Hardcode credentials in
AuthCallbackimplementations
MAY:
- Use
StealthConfigto customize anti-detection settings per site - Implement
is_authenticated()with an active API check instead of expiry-based - Pass
launch_kwargsfor site-specific browser configuration (e.g., proxy) - Use
force_authenticate()to bypass cache during debugging
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

