Reuse authenticated sessions across browsers

Maintain authenticated sessions across Steel browser instances by capturing and reusing cookies and local storage.

examples/auth-context-ts
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

An auth context is a snapshot of a browser's cookies and local storage at a point in time. Steel exposes one endpoint to read it and one session option to restore it:

// Capture: pull the current cookies + localStorage off a live session
const sessionContext = await client.sessions.context(session.id);
// Restore: hand the snapshot to a new session on create
const next = await client.sessions.create({ sessionContext });

The snapshot is plain JSON you can store, ship between machines, or diff. Restoring it into a fresh session means the new browser starts already signed in. No login flow, no password prompt, no captcha. Other recipes link here as the primitive for "start already authenticated."

How the demo works

index.ts runs the full round-trip against practice.expandtesting.com, a public login test site:

  1. 1Create session #1, connect Playwright over CDP, run the login helper to submit the form, run verifyAuth to confirm the welcome text.
  2. 2Call client.sessions.context(session.id) to pull the snapshot, then release session #1.
  3. 3Create session #2 with sessionContext set to that snapshot. Connect Playwright, run verifyAuth again without logging in. The welcome text is already there.

The second session is a brand new browser on Steel's fleet. It has the auth state because the snapshot restored it, not because anything is shared between sessions on the backend.

Run it

cd examples/auth-context-ts
cp .env.example .env # set STEEL_API_KEY
npm install
npm start

Get a key at app.steel.dev/settings/api-keys. The script prints two session viewer URLs as it runs. Open them in other tabs to watch each browser.

Your output varies. Structure looks like this:

Creating initial Steel session...
Steel Session #1 created!
View session at https://app.steel.dev/sessions/ab12cd34…
Initial authentication successful
Session #1 released
Steel Session #2 created!
View session at https://app.steel.dev/sessions/ef56gh78…
Authentication successfully transferred!
Session #2 released

A run takes ~20 seconds and costs a few cents of Steel session time. Both sessions go through client.sessions.release() in the finally block. Skipping it keeps browsers running until the 5-minute default timeout.

What's inside the snapshot

The shape returned from sessions.context() is an object keyed by origin, with cookies and storage entries for each. Treat it as opaque JSON for transport, and treat it as sensitive: it holds session tokens. Anyone with the blob can impersonate the logged-in user until those tokens expire.

Cookies expire. A snapshot captured today may not work next week, and rarely works next month. If you're persisting contexts to disk or a vault, refresh them on a schedule or re-authenticate on failure.

When to reach for this

Auth context fits one-shot flows where you already have a way to log in and just want to move the resulting state forward:

  • Log in once interactively, capture the context, run headless jobs against it.
  • Run an agent that signs in, snapshot at the end, hand the snapshot to the next agent in the pipeline.
  • Keep a single "warm" context in memory and spawn short-lived workers from it.

If you want Steel to store credentials and handle the login itself, see credentials. If you need a long-lived named identity that accumulates state across runs (history, extensions, preferences), that's a different primitive. The authentication topic collects the related recipes.

Make it yours

  • Swap the target site. Replace the URLs and selectors in login and verifyAuth. Everything between the sessions.context() capture and the sessions.create({ sessionContext }) restore stays identical regardless of site.
  • Persist the snapshot. Write sessionContext to a file or secret store after capture. Load it on the next run and pass it straight into sessions.create(). Treat the file like a password.
  • Re-auth on failure. Wrap verifyAuth on the restored session in a check: if it returns false, fall back to a fresh login and capture a new snapshot.

credentials · Playwright docs

examples/auth-context-py
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

Logging in is the expensive part of browser automation: forms, redirects, sometimes a captcha. Steel lets you do it once, freeze the result, and pour it into a brand new browser. main.py runs that whole loop with Playwright's sync API: log in on session #1, snapshot the auth state, throw session #1 away, then prove a fresh session #2 is already signed in without ever touching the login form.

The two calls that matter are a read and a write:

session_context = client.sessions.context(session.id)
session = client.sessions.create(session_context=session_context)

The round-trip is a no-op in Python

client.sessions.context() returns a Pydantic SessionContext model: cookies, local_storage, session_storage, indexed_db. The keyword session_context on create() wants a typed dict shaped the same way. You might expect to unpack and remap fields between them, but the SDK transforms the response model on the way in, so the object you capture goes straight back without a single field touched. Capture into a variable, hand the variable to create(), done. (The Go and Rust ports do have to copy fields between distinct read and write types. Python does not.)

That model is plain data. session_context.model_dump(by_alias=True) gives you JSON you can write to disk, push to a secret store, or move between machines. Treat it like a password: it holds live session tokens, and anyone holding the blob is the logged-in user until those tokens expire.

Browser lifecycle

The script starts one sync_playwright() driver and reuses it across both sessions, calling browser.close() after each so the CDP socket from the released session does not linger. The driver itself is stopped in finally alongside the session release, so a failure mid-run still tears everything down. Each session is reached through browser.contexts[0].pages[0], the page Steel opens for you, rather than new_page().

Run it

cd examples/auth-context-py
cp .env.example .env # set STEEL_API_KEY
uv run main.py

Grab a key at app.steel.dev/settings/api-keys. uv sync runs automatically on first uv run, so there is no separate install step. Prefer pip? pip install -e . then python main.py. Playwright needs its browser binaries once: playwright install chromium.

The script prints two session viewer URLs. Open them in other tabs to watch each browser live.

Your output varies. Structure looks like this:

Steel + Reuse Auth Context Example
============================================================
Creating initial Steel session...
Steel Session #1 created!
View session at https://app.steel.dev/sessions/ab12cd34...
Initial authentication successful
Session #1 released
Creating second Steel session with the captured context...
Steel Session #2 created!
View session at https://app.steel.dev/sessions/ef56gh78...
Authentication successfully transferred!
Session #2 released

A run takes about 20 seconds and costs a few cents of session time. Both sessions go through client.sessions.release(); session #2 is released in the finally block. Skip the release and the browser idles until the default timeout.

Make it yours

  • Swap the target site. Change the URLs and selectors in login and verify_auth. The capture and restore between them stay identical no matter the site.
  • Persist the snapshot. json.dump(session_context.model_dump(by_alias=True), f) after capture, load it next run, and pass the dict straight into session_context=. The keyword accepts the dict form too.
  • Re-auth on failure. If verify_auth on the restored session returns False, fall back to a fresh login and capture a new context. Cookies expire, so a snapshot from last week may already be dead.

TypeScript version covers the same flow as a reusable primitive. Go version and Rust version map fields between the read and write context types. If you want Steel to store credentials and run the login itself, see credentials. For the Playwright sync API, see the Playwright docs.

examples/auth-context-rs
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

A Steel auth context is the cookies and storage that make a browser "logged in." This recipe reads that snapshot off one session and hands it to the next, so the second browser starts already signed in. There is no login form on the second run.

One detail matters in Rust that the dynamic SDKs hide: the snapshot you read back is not the same type you write on create. client.sessions().context(&id) returns a SessionContext (its cookies are Vec<SessionContextCookie>), but SessionCreateParams::session_context wants a SessionCreateParamsSessionContext (cookies are Vec<SessionCreateParamsSessionContextCookie>). The two cookie structs carry the same fields under different struct names, so to_write_context in main.rs maps one into the other field by field. The compiler will not let you skip this.

What the demo does

main.rs drives practice.expandtesting.com, a public login test site, over CDP with chromiumoxide:

  1. 1Create session #1, connect, and run login: type practice / SuperSecretPassword! into the form and submit. verify_auth then loads /secure and checks that #username reads Hi, practice!.
  2. 2Read the snapshot with client.sessions().context(&session.id), then release session #1.
  3. 3Map the read snapshot into a SessionCreateParamsSessionContext, create session #2 with session_context set, connect, and call verify_auth again without logging in.

Each chromiumoxide connection spawns a handler task (tokio::spawn) to pump CDP events and handle.abort()s it before the session is released. The cookie map copies name and value (the required fields) plus the optional domain, path, expires, http_only, secure, same_site, priority, source_scheme, url, and session directly, since those types are shared between the read and write cookie structs; only partition_key is dropped. The local_storage and session_storage maps move across unchanged.

Run it

cd examples/auth-context-rs
cp .env.example .env # set STEEL_API_KEY
cargo run

Get a key at app.steel.dev/settings/api-keys. The run prints both session viewer URLs. Open them to watch each browser.

Creating Steel session #1...
Session #1 live at https://app.steel.dev/sessions/ab12cd34...
Logging in...
Initial authentication confirmed
Session #1 released
Creating Steel session #2 from the captured context...
Session #2 live at https://app.steel.dev/sessions/ef56gh78...
Session #2 released
Authentication successfully transferred without logging in

A run takes ~20 seconds. Both sessions go through client.sessions().release(...) before the program exits; skip it and the browsers idle until the 5-minute default timeout.

Make it yours

  • Swap the target. Change LOGIN_URL, SECURE_URL, and the selectors in login and verify_auth. The capture and replay around them stay the same for any site.
  • Persist the snapshot. SessionContext derives Serialize, so you can write it to disk or a vault after capture and load it on the next run. Treat the file like a password: it holds live session tokens.
  • Re-auth on failure. If verify_auth on the restored session returns false, fall back to a fresh login and capture a new snapshot. Cookies expire, so a snapshot from last week may already be dead.

auth-context-ts · auth-context-py · auth-context-go · credentials-rs · chromiumoxide

examples/auth-context-go
Contributors: Updated
Terminal

Scaffolds a starter project locally. Requires the Steel CLI.

A Steel session can hand you a snapshot of its browser state: cookies, localStorage, sessionStorage, indexedDB. Steel exposes that as one read call and one create option, so you log in once, pull the snapshot, and start a second browser that is already signed in.

// Capture the live cookies + storage off session #1
captured, _ := client.Sessions.Context(ctx, first.ID)
// Restore them into a brand new session #2
second, _ := client.Sessions.Create(ctx, steel.SessionCreateParams{
SessionContext: restoreContext(captured),
})

main.go drives both browsers with chromedp over CDP. It connects with chromedp.NewRemoteAllocator(ctx, cdpURL, chromedp.NoModifyURL) so the websocket URL Steel returns is used verbatim, then runs the login form on practice.expandtesting.com and reads the #username welcome text to confirm auth.

The read type is not the write type

This is the one sharp edge in the Go SDK. Sessions.Context returns a *steel.SessionContext with plain Go values: Cookies []steel.SessionContextCookie, LocalStorage map[string]map[string]string, and so on. The create side wants a steel.SessionCreateParamsSessionContext, where every field is wrapped in param.Field[...] and built with steel.F(...). So you cannot pass the captured value straight back in: you read concrete values and you write wrapped ones.

restoreContext does that bridge. It rebuilds each cookie into a steel.SessionCreateParamsSessionContextCookie, wrapping Name, Value, Domain, Path, Expires, HTTPOnly, and Secure with steel.F. The SameSite enum is the same named type on both sides (CreateSessionRequestSessionContextCookiesItemSameSite), so it just gets wrapped, not converted. LocalStorage and SessionStorage are the same map type on each side and pass through steel.F unchanged. If you only need cookies for your target site, you can skip storage entirely.

Run it

cd examples/auth-context-go
cp .env.example .env # set STEEL_API_KEY
go mod tidy
go run .

Get a key at app.steel.dev/settings/api-keys. The run prints two viewer URLs. Open them to watch each browser; the second one lands on the secure page without ever touching the login form.

Creating Steel session #1...
Session #1 live at https://app.steel.dev/sessions/ab12cd34...
Authenticated on session #1
Session #1 released
Creating Steel session #2 from the captured context...
Session #2 live at https://app.steel.dev/sessions/ef56gh78...
Authenticated on session #2
Authentication successfully transferred.
Releasing session #2...

Session #1 is released as soon as its context is captured. Session #2 is released by a defer on the way out, so a verify failure still cleans up. A full run is about 20 seconds.

Make it yours

  • Swap the target. Change the URLs and selectors in login and verifyAuth. The capture/restore path in restoreContext does not care what site you used.
  • Persist the snapshot. *steel.SessionContext marshals to JSON. Write it after capture, load it next run, feed it through restoreContext, and skip the login entirely. Treat the file like a password: it carries live session tokens.
  • Re-auth on failure. Cookies expire. If verifyAuth on session #2 returns an error, fall back to a fresh login and capture a new snapshot.

auth-context-ts · auth-context-py · auth-context-rs · credentials-go · chromedp docs