Fixtureframedocs
Adapters

Sessions

Signing in by hand once for a product behind Microsoft, Google or any other identity provider, keeping the session, and knowing what it can and cannot carry.

Some sign-ins should not be automated and some cannot be. A second factor that a machine can complete is not a second factor; a magic link means there is no password to type; and an identity provider's login page belongs to someone else, who redesigns it without telling you.

For those sign-ins, sign in interactively and save the session. You do not need an adapter or login-password variables. The saved state contains authentication tokens and stays local to the runner.

Capture the session

ff session init --url https://staging.acme.test

A browser opens at your product and waits. Sign in however it actually works — Microsoft, Google, Okta, a push notification on your phone, a code from an authenticator, a link in an inbox — then keep the browser open and press Enter in the terminal to save the session.

✓ Session captured — 19 cookie(s) across 4 domain(s).
  app.acme.test, login.microsoftonline.com, .login.live.com, .acme.test
  Earliest cookie lapses 2026-09-14T08:31Z.

  Next: `fixtureframe survey` now reaches what is behind the login.

Saving checks for cookies or stored browser state. That check does not establish that your app accepts the session: verify that the next survey or replay reaches the intended signed-in screen.

Two commands make the rest of it visible, because a file nobody mentions is a file people lose:

ff session status   # is there one, how old, when do its cookies lapse
ff session clear    # delete it and start again

ff record is not the way to do this, though it will also leave a session behind. Recording publishes a draft flow, replays it once as a run, and leaves artefacts to tidy up — a high price for signing in. Signing in is a precondition, not content. Use record when you want a walkthrough; it starts paused when nothing signed in for you, and --paused forces that even when an adapter would have.

If a coding agent is driving

The agent cannot do this part, and should not pretend to. session init opens a browser and waits for a person; an agent that runs it sits blocked for as long as you take, and most agent hosts cut a command off after a few minutes. It also refuses outright with no terminal attached, rather than opening a window nobody can see.

So the division is: the agent hands you the command, you run it in your own terminal and sign in, and the agent picks up from ff survey onwards. Survey restores it automatically; scripting and replay use it when you pass --session.

The same applies to record, for the same reason: when you do record a walkthrough, resume after the login, never before. Anything on screen while the recorder is running is captured, and a recorded password travels with the flow.

The same applies to anything that asks for a credential. The CLI prompts for secrets only when a terminal is attached. Run ff init yourself and enter the Studio API key in your terminal before handing the workspace to the agent. See the complete workflow.

By provider

The shape is the same everywhere; the interruptions differ.

ProviderWhat to expect
Microsoft (Entra ID)Email, then password on a second screen, then a "Stay signed in?" interstitial. Conditional access may add a device prompt.
GoogleAccount chooser, then password, then 2-step. Google often refuses to sign in inside an automated browser — see below.
Okta / Auth0 / OneLoginA hosted page, sometimes with a tenant or organisation step before the password.
SAML to a corporate IdPA redirect chain ending back at your product. Wait for your product's URL, not the IdP's.

Google is the awkward one. Google detects browsers under automation control and may answer with "This browser or app may not be secure" — which affects signing in by hand too, because the browser is still the one Playwright launched. If you hit that, the way through is a token rather than a login: see the token and api-key adapters.

What happens next time

The session is kept. Signing in by hand is a cost you pay once: it is written to .fixtureframe/auth/session.json — gitignored, disposable — and restored by the commands that would otherwise strand you at a login screen.

CommandSaved session
ff session initcreates one — that is all it does
ff recordrestores it, saves what you signed into, and its replay uses it
ff surveyrestores it, and saves
ff discover, ff scenariorestores it, and saves
ff scriptwith --session
ff run, ff run --checkwith --session
ff adapter checkwrites one on success, and never reuses one

Every command that opens a browser at your product can now use one. Three of them differ, each for a reason worth knowing.

ff script is opt-in because a script carries its own sign-in: steps that fill a login form fail against a browser already signed in, so restoring by default would break every script written before this existed. Pass --session for a product whose sign-in the script cannot perform.

ff run is opt-in because a run is an artefact other people trust. One that succeeded only because a session was sitting in this workspace would pass here and fail on a machine that has never signed in, reporting nothing unusual either time. Asking for it keeps the default reproducible.

A check takes the same flag, and it is the one worth reaching for on a product behind a login: a check captures nothing and calls no vendor, so it is what an empty balance still leaves you. Without a session it can only authenticate through an adapter, and lands on the sign-in screen reporting every step unresolved — which is true, and reads exactly like a broken flow.

ff adapter check never reuses one, deliberately. It answers "can this adapter sign in?", and yesterday's cookies would let a broken adapter pass for as long as they lived.

--fresh opts out on record and survey — for a walkthrough that means to show signing up, or a survey of the product as a signed-out visitor sees it.

ff check queues a sweep through the control plane rather than driving a browser itself, so the runs it queues follow the ff run rule above.

Fresh replay with saved authentication

A fresh replay starts a new browser context initialized with the saved session. It does not need a signed-out browser or username/password variables:

ff run .fixtureframe/flows/operator-tour.flow.json --local-only --session --base-url https://staging.acme.test

Inspect the run artifacts as well as its exit status. A successful sign-in and completed clicks do not establish that the walkthrough demonstrates its goal. The intended runner needs its own available authentication state; publishing a flow or data profile does not upload the session.

What a session can carry, and what it cannot

The file is a Playwright storage-state snapshot: the cookies the browser held, and the web storage each origin had.

{
  "cookies": [{ "name": "…", "value": "…", "domain": "…", "expires": 1789..., "httpOnly": true }],
  "origins": [{ "origin": "https://app.acme.test", "localStorage": [{ "name": "…", "value": "…" }] }]
}

Cookies span every domain the browser touched, the identity provider's included — which is what lets an SSO sign-in survive rather than only your application's own cookie.

Whether reuse works depends entirely on where your application keeps its session:

Where the session livesReused
Cookies — server-rendered apps, and most SSO landingsyes
localStorage — the usual single-page applicationyes
IndexedDB — Firebase Authentication, some MSAL configurationsyes
sessionStorageno
A token held only in memory, renewed silently in a hidden iframeno
Device-bound credentials, client certificates, HTTP authno
Anything the server pins to an IP, user agent or deviceno

sessionStorage is the one that catches people, and Microsoft in particular: MSAL's browser library defaults its cache to sessionStorage, which no snapshot can carry — a fresh browser context starts with it empty by definition. So a Microsoft-authenticated single-page app may refuse a restored session while a server-rendered app behind the same identity provider accepts one happily, because that one's session is a cookie.

Test reuse after ff session init with a survey or local replay and check that it opens signed in. restored the saved session means only that the file was applied — landing on your product rather than a login screen is the signal that it worked.

If your application is in one of the "no" rows, signing in by hand still works; it just works every time rather than once. The unattended routes — a token, or a test identity without a second factor — are then the only way to stop repeating it.

When the session expires

Sessions lapse, and the answer differs depending on how.

Lapsed by date. Every cookie's expiry is written in the file, so this is knowable before a browser opens. When every dated cookie has passed, the CLI says so and signs in again rather than pretending:

the saved session has expired — signing in again, and it will be saved when you do.

A single live cookie is enough to try, because products leave expired cookies beside working ones routinely. Cookies with no expiry at all — gone when the browser closes — are not judged either way.

Revoked, idled out, or invalidated by a password change. These look perfectly alive on disk, so the file cannot tell you and the product has to. What happens then depends on the command:

  • ff record recovers on its own. The product bounces to its login screen, the adapter signs in if it can, and if it cannot the recorder simply starts paused — you sign in again, and the new session is saved.
  • ff survey refuses. If the base URL redirects to another host, it stops rather than surveying somebody else's login page. Storing that as your environment's site map would poison every later ff lint, which would then report perfectly real names as unsurveyed — a quiet failure surfacing loudly, somewhere else, days later.

Saving never replaces a good session with an empty one. A browser closed on a login screen produces no cookies and no origins, and that is not written over the session a person earned with a code from their phone.

To renew a session that still looks valid on disk, run ff session init --url https://staging.acme.test --force in your terminal. Use ff session clear to remove it deliberately.

Which means, for anything that repeats

A saved session can support unattended replays while the app continues accepting it. Renew it interactively when it expires or is revoked. The flow remains reusable; it does not need to be recorded again just because authentication expired.

If the runner must renew authentication without a person, configure an appropriate adapter:

  1. A token. The token adapter exchanges configured credentials at your own auth endpoint and attaches the result; api-key carries a key issued once. Nothing is typed, nothing appears on screen, and a redesign of the login page cannot break it.
  2. A test identity without a second factor, signed in by form-login. Fine for a staging tenant you control, and the usual answer when the product's own login is a plain form behind SSO for staff only.

Document the chosen authentication method and renewal requirements so scheduled runs have the session or adapter they need.

Checking where you actually stand

ff adapter check <adapter> --url https://staging.acme.test --headed

Exit 0 and you have an unattended sign-in; you do not need this page. Exit 2 at the authenticate stage, with a frame showing an identity provider, and you are here.

On this page