ego (lite) è solo un browser; ego è il tuo agente personale su tutti i dispositivi.
Iscriviti alla lista d'attesa
PlaywrightAutenticazionestorageStateAutomazione del browserSessioni con accesso effettuato

Autenticazione in Playwright: accedi una volta, riutilizza la sessione e riautenticati quando scade

16 set 202614 min di lettura
Personaggi pixel art di Playwright e ego (lite) accanto a una schermata di accesso sul laptop, con Agent is in control visibile

Quando un test o uno scraper Playwright ha bisogno di autenticazione, di solito non c'è motivo di fare login da zero a ogni esecuzione. Un approccio comune è accedere una volta, salvare i cookie e lo stato correlato del browser con storageState, poi caricare quello stato nelle esecuzioni successive per riutilizzare la stessa sessione. Ma avere un file storageState salvato non significa che il login continuerà a funzionare all'infinito. La sessione può scadere, essere revocata dal server o non caricarsi correttamente fin dall'inizio.

Se stai automatizzando il tuo account e sei già autenticato nel browser che usi ogni giorno, c'è un'altra opzione: riutilizzare direttamente quella sessione browser esistente. ego (lite) consente agli agenti AI di lavorare in un browser che ha già lo stato di login di cui hanno bisogno, tenendo ogni attività isolata nel proprio Space. Se l'attività arriva a un codice di verifica, a un login QR o a un altro passo che richiede te, puoi prendere il controllo nel browser visibile invece di tentare di automatizzare l'MFA.

Qualunque approccio tu usi, l'obiettivo è lo stesso: assicurarti che l'automazione abbia davvero una sessione autenticata valida. Un'esecuzione che all'improvviso torna alla pagina di login, un'API che risponde 401 o un elemento successivo al login che non compare mai possono sembrare tre problemi distinti: navigazione, permessi o un problema di selettore, quando tutti e tre possono puntare alla stessa sessione rotta. Nelle sezioni seguenti useremo un setup locale ripetibile per mostrare come salvare, riutilizzare, verificare e rinnovare lo stato di autenticazione in Playwright.

Come appare un login Playwright rotto

Nell'esecuzione del fixture, una sessione revocata ha prodotto tutti e tre i sintomi insieme. Dopo la cancellazione della sessione lato server, un contesto che riutilizzava lo stato salvato è stato reindirizzato alla pagina di login con un parametro next che puntava di nuovo alla dashboard, una richiesta di probe all'endpoint JSON autenticato ha restituito 401 e l'attesa della tabella della dashboard è andata in timeout dopo i 3 secondi configurati.

Sul login pubblico di the-internet, la stessa classe di fallimento e il bounce. Aprire /secure senza una sessione viva finisce su /login con il flash rosso You must login to view the secure area. Quello e il controllo URL del riquadro sotto: il browser e sul form di login, non sulla pagina attesa dal locator. Questo screenshot e quel bounce. Non prova che Logout abbia cancellato un file storageState. Logout su the-internet chiude la sessione della finestra corrente; un file gia scritto su disco puo ancora riaprire /secure finche il cookie non e morto.

the-internet.herokuapp.com/login dopo una richiesta all area sicura, con flash rosso You must login to view the secure area
Aprire la pagina protetta senza una sessione viva e saltato a /login. Il flash rosso e il segnale. E una sessione assente o morta, non un locator rotto, e non Logout che cancella un file storageState salvato.

Il terzo sintomo è quello costoso. Un locator che va in timeout su un elemento successivo al login non dice che il locator è sbagliato; dice che la pagina che stai guardando non è quella che assumevi. Nell'esecuzione sopra la tabella non è mai stata renderizzata perché il browser era fermo sul modulo di login. Trattarlo come un problema di selettore porta a rattoppare una query che non era mai rotta.

Quattro cause radice dietro i fallimenti di autenticazione

I fallimenti di autenticazione si raggruppano in quattro cause, e ognuna ha una correzione diversa. Classificare prima di rattoppare è ciò che impedisce che la correzione sia un altro wait.

1. Lo stato non è mai stato caricato

Un contesto creato senza storageState parte disconnesso, qualunque cosa sia accaduta in un'esecuzione precedente. Lo stesso risultato arriva dal salvare il file nel momento sbagliato, dall'usare un percorso relativo che si risolve altrove o dall'eseguire lo step di setup in un progetto il cui stato non viene passato al progetto dipendente. Il segnale è che un contesto nuovissimo si comporta identico a quello che fallisce: entrambi sono disconnessi.

2. Il cookie c'è, ma è morto

Un cookie di sessione può esistere, portare il dominio e i flag giusti ed essere comunque rifiutato dal server. I cookie di sessione hanno una vita propria, e un server può revocarne uno in qualsiasi momento, incluso un redeploy, un cambio password o un idle timeout. Il campo expires del cookie è un indizio, non una garanzia: la sessione lato server può morire prima. Questo è il caso che il fixture riproduce con una chiamata revoke esplicita.

3. Isolamento tra contesti

Cookie e localStorage appartengono a un contesto del browser, non al browser. Salvare lo stato da un contesto e aspettarsi che un contesto configurato diversamente lo condivida fallisce in silenzio. Lo stesso vale per i worker paralleli: ogni worker ottiene il proprio contesto, quindi ogni worker ha bisogno del proprio file di storage o del proprio passo di login. Questo isolamento è una caratteristica. È anche il motivo per cui una sessione che funziona in un posto sembra sparire in un altro.

4. Catene di redirect SSO

Con il single sign-on, l'app che vuoi è di rado l'origine che detiene la sessione. Il login reindirizza a un identity provider su un altro dominio, il provider imposta i propri cookie e il controllo torna all'app con un codice o un token. Salvare lo stato prima che quella catena finisca cattura parte della sessione e perde i cookie dell'IdP, e l'esecuzione successiva riproduce il redirect. Il pattern affidabile è attendere l'URL finale dell'applicazione e un marcatore autenticato prima di salvare, così il file di stato porta ogni origine coinvolta nella catena.

Accedi una volta e salva lo stato correttamente

Il flusso di login una volta è breve: naviga, autentica, conferma lo stato autenticato, poi scrivi storageState su un file. Il passo di conferma è ciò che separa un file di stato che funziona da uno che a volte no.

import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

await page.goto("https://app.example.com/login");
await page.fill("input[name=email]", process.env.APP_EMAIL);
await page.fill("input[name=password]", process.env.APP_PASSWORD);
await page.click("button[type=submit]");

// Wait for the authenticated destination, not just for the click to land.
await page.waitForURL("**/dashboard");

// Verify before saving: an authenticated element plus a 200 from the API.
await page.waitForSelector("#revenue-table");
const probe = await context.request.get("https://app.example.com/api/summary");
if (probe.status() !== 200) throw new Error("login did not take");

await context.storageState({ path: "playwright/.auth/user.json" });
await browser.close();

Nell'esecuzione del fixture questo passo ha richiesto 119 millisecondi dall'apertura della pagina di login alla scrittura del file di stato. Il file conteneva un cookie, l'identificatore di sessione emesso dal server, e il probe ha restituito 200. Numeri così sono proprietà del fixture, ma la forma del controllo è ciò che si trasferisce: verifica l'URL di destinazione, verifica un elemento autenticato visibile, verifica un endpoint autenticato e solo allora persisti.

L esecuzione headed sotto ha usato the-internet.herokuapp.com, non la dashboard del fixture. I 119 millisecondi restano del fixture. Lo screenshot mostra la stessa forma di salvataggio su un login pubblico: /secure dopo l autenticazione, Logout visibile e Claude che verifica /tmp/pw-auth.json.

Claude Code accanto a una finestra Chromium headed su the-internet.herokuapp.com/secure dopo il login, con Logout visibile mentre Claude verifica /tmp/pw-auth.json
Chromium headed e rimasto su /secure dopo il login. Claude stava verificando /tmp/pw-auth.json. Quel file e cio che un contesto successivo riutilizza.

Per le suite di test, la stessa idea si esprime con un progetto di setup che produce il file e progetti dipendenti che lo consumano, il pattern documentato dalla guida ufficiale all'autenticazione. Per script e scraper, la sequenza sopra è l'intero flusso. In entrambi i casi il file è una credenziale: contiene cookie di sessione vivi, quindi trattalo con la stessa cura di una password e tienilo fuori dal controllo di versione.

Riutilizzare lo stato salvato in test e scraper

Il riuso è una modifica di una riga alla creazione del contesto. In uno script, passa il file al contesto. In una suite Playwright, impostalo per progetto o per file di test così ogni worker parte dallo stesso stato autenticato.

// Script: create the context from the saved state.
const context = await browser.newContext({
  storageState: "playwright/.auth/user.json",
});

// Test: use the state for this file (or configure it per project).
test.use({ storageState: "playwright/.auth/user.json" });

Nell'esecuzione del fixture, un nuovo contesto creato dal file salvato ha raggiunto la dashboard protetta in 6 millisecondi senza passo di login, e il probe autenticato ha restituito 200 con il payload atteso. Il numero interessante non sono i 6 millisecondi; è che nulla del flusso è cambiato tranne da dove proveniva lo stato del contesto.

Lo screenshot di riuso e lo stesso sito pubblico. Un nuovo contesto headed ha caricato /tmp/pw-auth.json, ha saltato il form di login e ha aperto /secure con Logout gia in pagina. I 6 millisecondi restano il tempo del fixture.

Claude Code che crea un nuovo contesto Playwright da /tmp/pw-auth.json senza compilare il modulo di login, accanto a the-internet.herokuapp.com/secure con Logout ancora visibile
Un nuovo contesto headed ha caricato storageState e ha aperto /secure direttamente. Il prompt diceva di non compilare il form di login; Logout era gia in pagina.

Vale la pena sapere cosa porta il file di stato e cosa no. Contiene cookie per ogni origine toccata dal contesto e voci localStorage per origine. Non contiene IndexedDB, session storage né stato dei service worker. Le applicazioni che tengono il token in IndexedDB hanno quindi bisogno di un passo extra di cattura o di un nuovo login; aspettarsi che storageState le copra è una fonte comune di fallimenti fantasma. I cookie stessi sono regolati da dominio, path e flag descritti in la guida cookie di MDN e il riferimento Set-Cookie.

Rilevare una sessione scaduta o revocata

Il rilevatore più economico è una richiesta autenticata fatta prima del lavoro costoso. La maggior parte delle applicazioni ha un piccolo endpoint che risponde con l'utente corrente, un conteggio o un oggetto di configurazione, e il suo codice di stato è il codice di stato della sessione. Una richiesta costa millisecondi; scoprire il fallimento a metà di uno scrape costa l'intera esecuzione.

async function sessionIsAlive(context) {
  const probe = await context.request.get(
    "https://app.example.com/api/summary",
    { failOnStatusCode: false },
  );
  return probe.status() === 200;
}

if (!(await sessionIsAlive(context))) {
  await loginAndSaveState(context);
}

Non affidarti solo al campo expires del cookie. Dice quando il browser deve smettere di inviare il cookie, che non è lo stesso momento in cui il server smette di accettarlo. La revoca lato server è invisibile finché non chiedi. La chiamata revoke del fixture è esattamente quella situazione: il cookie era ancora presente e ben formato, e il server ha risposto 401.

Riautenticarsi in automatico

La riautenticazione automatica è una piccola macchina a stati: rileva, rifai il login, aggiorna lo stato salvato, riesegui una volta il passo fallito e verifica. Il tetto conta. Un flusso che si riautentica in silenzio in un ciclo può mascherare una password sbagliata, un blocco account o una pagina di login che è cambiata, e nel frattempo genera traffico volentieri.

async function withSession(page, context, task) {
  if (!(await sessionIsAlive(context))) {
    await loginAndSaveState(context, page); // refresh file after login
  }
  try {
    return await task();
  } catch (error) {
    if (!(await sessionIsAlive(context))) {
      await loginAndSaveState(context, page); // one retry, then fail loudly
      return await task();
    }
    throw error;
  }
}

Nel fixture, il rilevatore ha visto il 401, il nuovo login e la verifica sono terminati in 99 millisecondi, e il file di stato aggiornato conteneva di nuovo esattamente un cookie di sessione. L'esecuzione è finita sulla dashboard protetta con la tabella visibile e tre righe renderizzate. Una riautenticazione che termina senza quelle tre conferme non è finita.

Se più worker condividono un file di stato, possono tutti notare la scadenza nello stesso momento e gareggiare per rifare il login. Il rimedio è un coordinamento single-flight: il primo worker aggiorna il file e gli altri aspettano, oppure ogni worker aggiorna la propria copia. La meccanica è la stessa dei problemi di persistenza della sessione che compaiono quando più agenti AI girano contro lo stesso profilo del browser.

Più account ed esecuzioni in parallelo

L'isolamento è per contesto, quindi la regola è un file di storage per account o ruolo, un contesto per worker e nessuna sessione mutabile condivisa. Nel fixture, due login in parallelo hanno prodotto due cookie di sessione diversi, entrambi i probe hanno restituito 200 nello stesso millisecondo e nessun contesto poteva vedere la sessione dell'altro. Questa è la proprietà da preservare di proposito, perché il modo di fallire quando si rompe è sottile: due account si sovrascrivono lo stato e un test passa per l'utente sbagliato.

La stessa regola vale in un browser reale. Due Space sono due contesti: uno puo restare idle su una nuova scheda mentre l altro tiene una sessione Airbnb autenticata, senza condividere i cookie come farebbe una sola finestra headed. Il punto e l isolamento. La panoramica serve solo a vederli insieme.

Claude Code accanto alla panoramica Spaces di ego (lite) con uno Space Google idle e uno Space Airbnb autenticato in esecuzione
Uno Space Google idle accanto a uno Space Airbnb autenticato in esecuzione. L isolamento e visibile; la scheda idle non e una seconda ricerca autenticata.

Verificare che l'autenticazione abbia avuto effetto

La verifica sono tre controlli, e saltarne uno è il modo in cui un flusso resta rotto in silenzio. Il primo è positivo: un elemento che esiste solo dopo il login è presente. Il secondo è negativo: il modulo di login è assente, così una pagina che per caso contiene entrambi non viene accettata. Il terzo è un controllo sui dati che fallirebbe su una pagina stantia o in cache, ad esempio un valore che deve cambiare tra un'esecuzione e l'altra.

await page.waitForSelector("#revenue-table");      // positive
await expect(page.locator("#login-form")).toHaveCount(0); // negative
const probe = await context.request.get("/api/summary"); // data
assert.equal(probe.status(), 200);
assert.ok((await probe.json()).q3Revenue);

Tutti e tre sono passati nell'esecuzione di riuso del fixture: la tabella era visibile, il conteggio del modulo di login era zero e il payload JSON conteneva il campo atteso. I tre insieme richiedono pochi millisecondi e trasformano la differenza tra una sessione che funziona e una rotta da un'inferenza in un fatto. Altre tecniche di debug per le esecuzioni che ancora si comportano male sono documentate nella guida al debug di Playwright, e la pagina delle best practice copre le abitudini circostanti, incluso quali attese vale la pena scrivere.

Quando la sessione appartiene al tuo browser reale

Un file storageState di Playwright è qualcosa che possiedi. È lo strumento giusto quando l'account è un fixture, la password sta nei secret di CI e nessuno è seduto alla macchina. È lo strumento sbagliato quando il login è tuo: SSO, una chiave hardware, un prompt push, una sessione che tieni già calda nel Chrome di tutti i giorni. In quel caso il lavoro non è ricostruire il login. È prendere in prestito il browser che già ce l'ha.

ego (lite) è quel prestito. Un agente AI può aprire una scheda già autenticata che usi già, continuare a lavorare in uno Space che non ti ruba il mouse e fermarsi quando un secondo fattore o una conferma di pagamento ha bisogno di te. La sessione non diventa mai un file JSON su disco. Questo è il punto: un login personale deve restare nel browser, non viaggiare attraverso un percorso storageState che in CI finirebbe committato per sbaglio.

Claude Code che mostra un confronto di annunci Airbnb completato accanto a uno Space ego (lite) live su airbnb.com.sg, con Agent is in control e Take over visibili
La sessione Airbnb autenticata e restata nel browser. L agente ha lavorato in uno Space con Agent is in control e Take over visibili.

Tieni i due strumenti distinti. Gli account di test non presidiati restano su storageState, come descrive la guida all'autenticazione di Playwright. Le dashboard personali restano in un browser visibile. ego (lite) non clicca l'MFA, non conferma i pagamenti e non raccoglie credenziali; questi stop sono documentati nella documentazione Space e nella guida rapida. La versione attuale del prodotto è 0.5.0.32 (changelog, 2026-09-12). Ricontrolla il changelog e il repo GitHub prima di citare un build più nuovo.

FAQ

Perché Playwright finisce ancora sulla pagina di login dopo aver salvato storageState?

Playwright finisce ancora sulla pagina di login dopo storageState quando il file è stato salvato troppo presto, il contesto non l'ha mai caricato o il server ha revocato il cookie. Controlla prima i cookie del file, poi le opzioni del contesto. Se entrambi sembrano corretti, trattalo come una sessione morta, non come un bug di navigazione.

I cookie di sessione sopravvivono a storageState?

Sì. Il file registra i cookie abbiano o no una scadenza, insieme a localStorage per origine. Ciò che non sopravvive è tutto ciò che il browser tiene fuori da quella struttura: IndexedDB, session storage, cache e service worker.

Quanto dura un login Playwright salvato?

Finché il server continua ad accettare la sessione, una decisione che il server prende e può invertire in qualsiasi momento. La data di scadenza del cookie è un tetto, non una promessa. Tratta qualsiasi file di stato longevo come stantio finché un probe non dice il contrario.

I worker paralleli possono condividere un file storageState?

Possono leggerlo, e il contesto di ciascun worker resterà isolato. Il problema compare quando un worker aggiorna il file dopo un nuovo login mentre un altro è a metà esecuzione. O dai a ciascun worker la propria copia, o serializza l'aggiornamento così avviene un solo login alla volta.

Come mi autentico con OAuth o SSO in Playwright?

Completa una volta l'intera catena di redirect, in un contesto che terrà i cookie dell'identity provider, e salva lo stato solo dopo aver raggiunto l'URL finale dell'applicazione. Se il provider richiede un passo interattivo, fallo una volta a mano nella stessa esecuzione e riutilizza in seguito lo stato risultante.

Come vanno gestiti MFA e codici monouso?

Non automatizzando il secondo fattore con un codice memorizzato. Il pattern onesto è un passo umano una tantum il cui risultato è la sessione salvata, oppure una sessione già autenticata in un browser che possiedi. Conservare segreti OTP o codici SMS accanto a un file di storage converte un controllo di sicurezza in una passività.

È sicuro fare commit di storageState nel controllo di versione?

No. Il file contiene cookie di sessione vivi. Tienilo nella directory di lavoro, ignoralo in git e in CI iniettalo da uno store di secret. Se è mai stato committato, tratta la sessione come filtrata e revocala.

Come testo di proposito che la mia automazione gestisce una sessione scaduta?

Aggiungi un hook di test che invalida la sessione lato server, come fa l'endpoint revoke del fixture, ed esegui contro di esso il percorso di riuso. Asserisci il redirect, il 401 del probe e il recupero. Quel singolo test copre il percorso di codice che altrimenti scatta solo in produzione.

E se l'app memorizza il token in IndexedDB?

storageState non lo porterà, quindi usa uno di due percorsi: cattura il token con uno script di pagina dopo il login e re-iniettalo al riuso, oppure autenticati in ogni esecuzione usando l'API che emette il token. Il primo è più veloce, il secondo è meno accoppiato agli interni dell'app.

Ogni esecuzione dovrebbe riautenticarsi invece di riutilizzare lo stato?

Per CI con un account di test usa-e-getta, riautenticarsi a ogni esecuzione è il default più sicuro. Per un job che gira spesso contro una sessione stabile, riuso più un probe e un nuovo login con tetto è più economico e più facile da ragionare. Entrambi sono legittimi; il riuso non verificato non lo è.

Perché il login funziona in locale ma fallisce in CI?

Di solito perché il file di stato manca o è più vecchio in CI, l'orologio differisce, o la pagina di login serve una variante diversa a un IP nuovo. Riproduci con la stessa impostazione headless, lo stesso viewport e lo stesso file di storage, e sonda la sessione prima che parta la suite. Il fallimento è spesso ambientale, non una differenza di codice.

Un file storageState può servire due account?

No, e i tentativi di unirli tendono a tenere il cookie scritto per ultimo. Usa un file per account o ruolo, nominalo in base all'identità a cui appartiene e passa quello giusto a ciascun contesto.