Skip to main content
Info

In short: HTTP has no memory, so after you prove who you are once (a password, a code, a fingerprint), the site hands your browser a token (a cookie, or a value your page keeps) and the browser sends it back with every request. That token lives in state the browser keeps: cookies, localStorage, sessionStorage and IndexedDB, all inside a browser profile. Everything about logging in is a variation on that: how you prove yourself, what the token is, and where it lives. Keep the profile, or save the state and load it into another browser, and the site sees the same signed-in user without a new login, so a test that understands this can skip the slow part.

HTTP forgets everything#

The web’s transport, HTTP, is stateless: each request is a self-contained question, and the server answers it without remembering the last one. Ask for /inbox twice and, as far as the protocol is concerned, two strangers asked.

Every feature that needs memory (a login, a shopping cart, a language choice, a “don’t show this again”) has to be added on top. The browser carries the memory and hands it back with each request. There are two places it can live:

  • In the request itself, as a cookie: a small piece of text the browser sends automatically, so the server sees it on every request.
  • In the page’s storage, as web storage: data the page’s own JavaScript reads and writes. The server sees it only if the script chooses to send it.

Cookies, in depth#

A cookie is born when a server answers with a Set-Cookie header. The browser stores it, and sends it back in a Cookie header on later requests that match:

Everything after the first name=value pair is an attribute that tells the browser when the cookie applies and how to protect it:

AttributeWhat it does
DomainWhich hosts get the cookie. Domain=example.com covers example.com and every subdomain. Without it the cookie is “host-only”: it goes to the exact host that set it and to no subdomain.
PathOnly URLs under this path get the cookie. Path=/ means the whole site; Path=/admin means /admin and below.
Expires and Max-AgeHow long the cookie lives. Expires is a date; Max-Age is seconds from now (and wins if both are present). Without either it is a session cookie and disappears when the browser closes. With one it is persistent and survives restarts.
SecureSend it only over HTTPS.
HttpOnlyHide it from JavaScript: document.cookie cannot read it. This protects a login cookie from being stolen by an injected script. The browser still sends it.
SameSiteWhether the cookie is sent on requests that start from another site. Strict: never. Lax (the default in modern browsers): only on top-level navigations such as following a link. None: always, but then Secure is required.
PartitionedStore this third-party cookie separately for each top-level site (see below).

Cookie names can also carry a prefix that the browser enforces: __Secure- requires Secure, and __Host- additionally requires Path=/ and no Domain, which pins the cookie to one host.

First-party and third-party#

A cookie is first-party when it belongs to the site shown in the address bar and third-party when it is set by something embedded in the page: an ad, a video player, a “like” button. The same third-party service embedded on many sites can recognise you on all of them, which is how cross-site tracking works. Browsers have been closing that door in different ways: blocking third-party cookies, applying SameSite=Lax by default, and partitioning (each top-level site gets its own copy of the third-party cookie, so the embedded service cannot connect your visits).

“Same site” has a precise meaning: the registrable domain, the part of the host you can buy, found with the Public Suffix List. shop.example.com and www.example.com are the same site; example.com and example.org are not; alice.github.io and bob.github.io are different sites, because github.io is on the list.

Limits and the rest#

Browsers cap a cookie at about 4 KB and the number of cookies per domain (Chrome: about 180), dropping the oldest. Cookies are also not private to a port or to http versus https in the way origins are, which is part of why they are easy to misuse. A cookie is the key to the account: if a server uses a random sid value as proof of login, whoever holds that value is that user.

Web storage#

Cookies are small and travel on every request. For larger data that only the page needs, browsers provide storage APIs. They are scoped to an origin (defined below).

StorageSizeLifetimeShared between tabsSent to the server
localStorageAbout 5 MBUntil deletedYes, all tabs of the originNever
sessionStorageAbout 5 MBUntil the tab closesNo, one tab onlyNever
IndexedDBLarge (a share of the disk)Until deletedYesNever
CookiesAbout 4 KB eachUntil they expireYesWith every matching request

localStorage and sessionStorage hold strings as key and value pairs and are synchronous. IndexedDB is a real database of objects, with indexes and transactions, and is asynchronous. Single-page apps often keep their session token, user settings or an offline copy of data in these places, not in cookies, so saving only the cookies may not be enough to restore a login.

Two more places hold state: the HTTP cache (files the browser reuses instead of downloading again) and service workers with their Cache Storage (a script that can answer requests itself, which is how sites work offline).

Origins and the same-origin policy#

An origin is the triple of scheme, host and port, and two URLs are the same origin only if all three match. The comparison table is in Origins: the unit of trust.

The same-origin policy is the browser’s main security rule: a page can read data only from its own origin. A script on evil.example cannot read your localStorage for bank.example, cannot read the response of a request to bank.example, and cannot reach into a bank.example iframe. Sites that want to share data on purpose use CORS headers (Access-Control-Allow-Origin) or postMessage.

This rule explains two behaviours you will meet in automation:

  • Storage belongs to the origin, so you must be on a page of that origin before you can read or write its localStorage.
  • A cookie belongs to a domain, so a cookie for example.com can only be added while the browser can attribute it to that domain.

Sessions and tokens#

Authentication answers “who are you?”. Authorization answers “what may you do?”. They are different checks, and mixing them up is how broken access control happens. The sections from here on are about the first.

After a login the server needs a way to recognise you on the next request. There are two main designs.

Server-side sessionSelf-contained token (such as a JWT)
What the browser holdsA random session ID, usually in a cookieA signed token with the user’s details inside
Where the facts areIn a store on the serverInside the token itself
Signing outDelete the session on the server; it is dead at onceHard: the token is valid until it expires, unless the server keeps a block list
ScalingThe server needs a shared session storeAny server can check the signature
Typical home in the browserA cookie with HttpOnlyA cookie, or localStorage and an Authorization header

Where the token lives matters. A cookie marked HttpOnly cannot be read by page JavaScript, so a cross-site scripting bug cannot steal it. A token in localStorage can be read by any script on the page. The sections above show how cookies and storage behave.

How a login persists#

The password is sent once. The server hands back an opaque session id, the browser attaches it to every later request, and the server looks the id up. Some sites keep a signed token (a JWT) in a cookie or in localStorage instead; a “remember me” login is a persistent cookie with a long Max-Age. The server decides when the login ends: the session record can expire, be revoked, or be tied to details such as the IP address or the browser. A saved state that worked yesterday can therefore stop working even though the cookie is unchanged.

Until it ends, though, the cookie is the login: copy it into another browser and that browser is logged in. That is the basis of the fast test logins below.

HTTP Basic authentication#

Basic authentication is built into HTTP. The server answers 401 with a header WWW-Authenticate: Basic realm="...", the browser pops up its own username and password box, and from then on it sends

which is the Base64 of user:password. That is an encoding, not encryption (see encryption), so Basic is safe only over https.

For automation the awkward part is the native dialog: it is not part of the page, so no selector can reach it. The ways around it are to put the credentials in the address (https://user:pass@host/, accepted for the first navigation by most browsers), to send the Authorization header yourself by intercepting requests (network), or to answer the Fetch.authRequired event of the DevTools Protocol. Bearer tokens use the same header with a different word: Authorization: Bearer <token>.

OAuth 2.0 and OpenID Connect#

“Sign in with Google” uses two standards. OAuth 2.0 lets an app get limited access to your account without ever seeing your password. OpenID Connect (OIDC) adds an ID token on top, so the app also learns who you are.

The usual flow is the authorization code flow with PKCE:

The browser is sent away to the provider and back. The password is typed on the provider’s page only.

Three terms help: the state value stops a forged return; PKCE stops someone who steals the code from using it; and the scope names what access is asked for.

Tests and OAuth. The redirect chain crosses several origins, so a test must wait for the browser to come back to the app rather than act at once. Do not automate a login on a real public provider: they detect bots, ask for MFA and may lock the account. Use a provider test tenant, a fake identity provider that your suite starts, or skip the redirect altogether with the fast login below.

JWTs#

A JSON Web Token is a compact, signed note. It is three Base64URL parts joined by dots: a header, a payload and a signature.

The payload holds claims: sub (who), iss (who issued it), aud (who it is for), iat and exp (issued and expiry times, in seconds since 1970).

  • It is signed, not encrypted. Anyone can read the payload by decoding it, so never put a secret in a JWT.
  • The server must check the signature, the expiry and the audience, and must refuse the none algorithm. Skipping any of these is a classic hole.
  • Access tokens are short (minutes) and are renewed with a longer-lived refresh token.

Multi-factor authentication and TOTP#

MFA asks for more than one kind of proof: something you know (a password), something you have (a phone, a key) and something you are (a fingerprint).

The common “have” is a TOTP (time-based one-time password), the six digits in an authenticator app, defined in RFC 6238. When you enrol, the site shows a QR code that holds a shared secret (a Base32 text). From then on both sides compute the same value:

A new code appears every 30 seconds and the server accepts the current one (often the neighbouring ones too, for clock drift). Nothing travels between phone and server; only the shared secret and the clock matter.

For a test account this is good news: a TOTP is just a calculation. Keep the Base32 secret of the test account in your secret store and compute the code when the login asks for it. Two practical rules: the machine’s clock must be right, and if the current window has only a second or two left, wait for the next one, or the code will have expired by the time it is submitted. Codes sent by SMS or email need a test inbox or number; push approvals (“tap yes on your phone”) cannot be automated at all, so ask for a test account that uses TOTP.

Passkeys#

A passkey replaces the password with a key pair, using the WebAuthn standard. The device creates the pair when you register; the private key never leaves it (and is unlocked by a fingerprint, face or PIN), and the site stores only the public key. To log in, the site sends a random challenge, the device signs it, the site checks the signature. There is no password to steal or phish, and the key works only for the site it was made for.

For tests the unlock step is a native system dialog, which automation cannot click. The DevTools Protocol has a WebAuthn domain with virtual authenticators: a fake device inside the browser that approves requests on its own. See Passkeys and WebAuthn for the commands.

Profiles, and what is in one#

A browser profile is a folder on disk, the user data directory (--user-data-dir). It holds the cookie database, localStorage, IndexedDB, the cache, saved passwords, extensions and settings. If you start Chrome again with the same folder, it is the same user: still logged in, same history.

Two rules follow:

  • A fresh profile is a fresh visitor. The default for automation: every run starts with an empty temporary profile and the site sees a first visit.
  • One browser per profile folder. Chrome locks the folder, so two browsers cannot share it at the same time.

Chrome also has two modes that never write a profile to disk: incognito (a temporary profile that is thrown away) and guest (a clean profile with no sign-in).

Contexts: isolation without a second browser#

A browser context is the DevTools Protocol’s name for what Chrome shows as an incognito window: a set of tabs with their own cookies, storage and cache, sharing nothing with any other context in the same Chrome process. It is far cheaper than starting another browser, which makes it the right unit for “one clean user per test” and for running many users in parallel.

Logging in fast in tests#

A UI login can take seconds and may involve a redirect chain, MFA and rate limits. Doing it before every test is slow and fragile. Because the cookie is the login, you can do it once and reuse it.

Log in once

Either through the real page, or by calling the login API directly with an HTTP client and keeping the Set-Cookie values.

Keep the proof

Save the cookies (and the localStorage entries, for token-in-storage apps) to a file, or to a store that other workers share.

Inject it into each test

Open any page of the right site, put the cookies into the browser, and reload. The test starts already signed in, on the page it is about.

ApproachSpeedRealismWatch out for
Log in through the UI in each testSlowestHighestMFA, rate limits, redirect chains
Log in through the API, inject the cookieFastGoodThe API login must create the same cookie as the page
Save a UI login once, reuse itFastHighExpired sessions: save again when a cookie runs out
Pre-made test session in the app (a test-only endpoint)FastestLowerNever ship that endpoint to production
Warning

Keep at least one test that logs in through the real page. Fast logins prove the rest of the app works for a signed-in user; they prove nothing about the login. Also treat a saved cookie file like a password: it is a password until it expires. Keep it out of Git and out of CI logs.

Saving a signed-in state#

These are the calls for each step. To save the state, use save_cookies in BaseCase, page.cookies().save(path, filter) for a file in the pure CDP engine, or save_session on a pool lease for an in-memory copy (and the localStorage entries as well, where the site keeps its token there). To load it, open the site’s origin first, because cookies belong to a domain, then call load_cookies, page.cookies().load(path, filter) or load_session.

The in-memory form is a Session: the cookies plus the localStorage entries of one origin, built with Session::new(cookies) and with_local_storage(origin, entries). A SessionStore keeps named sessions for as long as the process runs; it does not write to disk. Both drop what has expired: Session::without_expired(now) and SessionStore::prune_expired(now), where now is a Unix time in seconds, the unit a Cookie’s expires field uses.

Warning

Saved cookies are credentials: anyone with the file is signed in as you. Keep them out of version control. With the turso feature, ProfileVault stores browser profiles encrypted; see Results database and encrypted profiles.

How seleniumbase-rs uses it#

Sessions in each engine#

A WebDriver session is one browser started by the driver, with one profile. sb.quit() ends it. --reuse-session (SB_REUSE_SESSION) shares browsers between the tests of a run, so later tests start where earlier ones left off.

Cookies and storage have their own methods: get_cookie, add_cookie, delete_cookie, save_cookies and load_cookies to a file, and get_local_storage_item, set_local_storage_item and their session-storage twins. See Cookies and storage.

Logins, TOTP and saved sessions#

BaseCase computes the code for you:

  • get_totp_code(secret) returns the current six-digit code for a Base32 secret. get_mfa_code and get_google_auth_code are aliases of it. The code generator is checked against the RFC 6238 test vectors, ignores spaces, dashes and letter case in the secret, and refuses text that is not Base32.
  • enter_totp_code(css, secret) generates the code, types it into the element that css selects and presses Enter. enter_mfa_code is an alias. This matches Python’s enter_mfa_code(selector, totp_key).
src/main.rs

Where to go next#