Gestione dei cookie cross-origin tra WebView e API native in React Native

cookie_reactnative

12 minuti di lettura

Una WebApp caricata dentro una WebView dà l’impressione di essere parte integrante dell’app mobile. L’utente effettua il login una sola volta, vede le stesse pagine ovunque, e si aspetta che ogni funzione — anche quella nativa — riconosca la propria sessione senza doverla ripetere.

Dal punto di vista tecnico, però, il login può produrre credenziali destinate a contesti completamente diversi tra loro: il cookie serve alla sessione web e viene utilizzato dalla WebView; il JWT serve alle chiamate native e viene custodito nel Keychain o nel Keystore; il refresh JWT serve a ricreare la sessione quando l’app viene riaperta; e le chiamate API eseguite con fetch o Axios non sono affatto automaticamente equivalenti alle richieste della pagina web.

È qui che nasce il problema dei cookie cross-origin tra WebView e API native. Il punto non è soltanto configurare CORS correttamente: bisogna coordinare due ambienti di esecuzione, due flussi HTTP, e più forme di autenticazione che convivono nello stesso momento.

Nel progetto myGenius la soluzione è stata costruita attorno a @react-native-cookies/cookies, a un servizio dedicato per il controllo della sessione, e al bridge tra WebApp e shell React Native. In questo articolo vediamo il problema, l’implementazione, e il bug più insidioso di tutti: un JWT scaduto mentre il cookie della WebView sembra ancora, ingannevolmente, valido.


Il problema: una sessione, due ambienti

In un browser tradizionale, il cookie viene conservato dal browser stesso, e allegato automaticamente alle richieste verso il dominio corretto. In un’app React Native esistono invece almeno due livelli separati: la WebView, che esegue la WebApp e gestisce la navigazione web; e il codice nativo, che usa fetch, Axios e i servizi React Native per chiamare le API.

Anche quando entrambi comunicano con https://my-genius.it, non è corretto dare per scontato che condividano lo stesso stato di sessione. La WebView ha il proprio contesto web, mentre il codice nativo accede alla cookie store tramite una libreria dedicata, oppure deve inviare esplicitamente le credenziali a ogni richiesta.

Per questo il termine “cross-origin” va interpretato qui con attenzione. Non indica necessariamente una richiesta browser proveniente da un dominio completamente diverso. Nel caso di una WebView, il problema principale è spesso la condivisione dello stato tra runtime e cookie store differenti: CORS può essere un ulteriore vincolo per le richieste browser, ma non sostituisce affatto la sincronizzazione dei cookie lato app.

Il sintomo tipico è comunque facilmente riconoscibile: la WebApp mostra l’utente come autenticato, ma una chiamata nativa restituisce 401 Unauthorized — oppure, esattamente al contrario, una chiamata nativa funziona regolarmente ma la WebView torna inaspettatamente alla pagina di login. La causa non è quasi mai una password errata. È molto più spesso un cookie presente nel posto sbagliato, una scadenza incoerente tra i due canali, o un token che semplicemente non è stato aggiornato dopo il refresh.



Perché credentials: include non risolve tutto

Quando si usa fetch sul web, credentials: 'include' indica che la richiesta può includere credenziali come i cookie, secondo le regole del browser e del server coinvolti.

In React Native, però, questa opzione non è affatto una soluzione universale per collegare automaticamente i cookie ricevuti da Axios, i cookie della WebView, i cookie letti o scritti tramite CookieManager, e le richieste native eseguite da runtime diversi tra loro.

Per questo myGenius ha un servizio separato, src/services/CookiesService.js, che centralizza il controllo della sessione:

const wpApiUrl = 'https://my-genius.it';

export const checkLoginStatus = async () => {
  try {
    const authUrl = `${wpApiUrl}/wp-json/genius/v1/check-auth/`;

    const authResponse = await fetch(authUrl, {
      method: 'GET',
      credentials: 'include',
    });

    const authData = await authResponse.json();
    return !!authData?.logged_in;
  } catch (error) {
    return { error: error.message };
  }
};

Il servizio non si limita a verificare se esiste una chiave nella cookie store: interroga direttamente il backend, e chiede se la sessione è ancora davvero valida. È una distinzione fondamentale da tenere presente — la semplice presenza fisica di un cookie non dimostra affatto che il server lo accetti ancora.

AuthService.checkLoginCookie() combina infatti i due controlli in un’unica verifica: cerca un cookie wordpress_logged_in_*, e interroga checkLoginStatus(). Solo la presenza del cookie e una risposta positiva del backend, insieme, dovrebbero consentire di aprire la WebView autenticata.

In una versione più robusta, anche il ramo di errore dovrebbe restituire un valore booleano coerente — per esempio false — invece di un oggetto errore generico. In questo modo chi consuma checkLoginStatus() non rischia di interpretare per sbaglio un oggetto come valore truthy, soprattutto in caso di errore di rete improvviso.




Configurare la WebView

La WebView deve essere configurata per utilizzare la cookie store condivisa prevista dalla piattaforma:

<WebView
  source={{ uri: webviewUrl }}
  sharedCookiesEnabled
  thirdPartyCookiesEnabled
  javaScriptEnabled
  domStorageEnabled
  onMessage={onMessage}
/>

sharedCookiesEnabled è importante perché abilita l’uso dei cookie condivisi per le richieste della WebView, in particolare nel comportamento iOS documentato da react-native-webview. thirdPartyCookiesEnabled, invece, riguarda la policy dei cookie di terze parti, soprattutto su Android — e non va confuso con la sincronizzazione del cookie principale di my-genius.it: abilitarlo non sostituisce CookieManager.set, e non risolve da solo una configurazione errata di dominio o scadenza.

La sequenza corretta dopo un login nativo è quindi questa: prima l’API di login, poi la risposta con JWT e cookie/sessione, poi CookieManager.set o setFromResponse, poi la WebView con sharedCookiesEnabled attivo, e solo a quel punto il caricamento della WebApp autenticata.

Se il cookie viene scritto dopo che la WebView ha già iniziato il caricamento, può essere necessario ricaricare la pagina, oppure controllare con più attenzione il momento in cui viene aperta la route WebView. La sincronizzazione deve sempre avvenire prima della prima richiesta autenticata della pagina, non dopo.

La libreria @react-native-cookies/cookies è il punto di integrazione usato oggi dal progetto, ma va considerata a tutti gli effetti una dipendenza infrastrutturale da monitorare nel tempo. Prima di un nuovo progetto, o di un aggiornamento importante, è opportuno verificarne compatibilità, manutenzione e comportamento sui dispositivi realmente supportati.


Sincronizzare il JWT con il Keychain

Il secondo canale passa dal bridge WebView–React Native. Quando la WebApp deve consegnare un JWT alla shell nativa, invia un messaggio tipizzato:

window.ReactNativeWebView.postMessage(JSON.stringify({
  type: 'jwt_token',
  token: jwt,
}));

Il controller nativo riceve il messaggio, e salva il token tramite il servizio di autenticazione:

if (message.type === 'jwt_token') {
  await saveAuthToken(message.token);
}

Il controller non deve validare manualmente la firma del JWT, e non deve stampare il valore nei log per nessun motivo. Il suo compito è trasferire la credenziale al servizio responsabile della persistenza — nient’altro. La validazione effettiva resta sempre responsabilità del backend e del flusso di autenticazione a monte.

Il refresh_jwt segue invece un percorso distinto. Dopo il login OTP, l’hook dell’identificativo propone all’utente di abilitare l’accesso biometrico; se l’utente accetta, il refresh token viene salvato nel Keychain tramite saveJWT. All’avvio successivo, l’app legge il refresh token dal Keychain, chiama /jwt-login, riceve una nuova risposta di autenticazione, persiste gli eventuali cookie restituiti, e apre la WebView soltanto se il backend conferma davvero il login.

In questo modello, cookie e JWT non vengono mai fusi in un’unica credenziale indistinta. Vengono sincronizzati nello stesso flusso, ma mantenendo comunque responsabilità ben distinte tra loro.


Il bug della scadenza: cookie valido, token scaduto

Il bug più difficile da diagnosticare, in assoluto, nasce quando si confondono due scadenze concettualmente diverse.

Nel JWT, la claim exp indica il momento oltre il quale il token non deve più essere accettato. Secondo RFC 7519 è un NumericDate, espresso in secondi Unix. Se il server considera il token scaduto, nessuna modifica locale alla data del cookie potrà mai renderlo nuovamente valido — il controllo avviene sempre lato server, non lato client.

expires nel cookie, invece, indica fino a quando il client deve conservare e inviare quel cookie. È una regola della cookie store, non un’estensione della validità del JWT — sono due meccanismi completamente separati, anche se il nome può suggerire il contrario.

Questo scenario, per esempio, è del tutto incoerente:

JWT:    scade alle 10:00
Cookie: scade tra un anno

Alle 10:05 il cookie può essere ancora presente e apparentemente valido, ma il backend può comunque rifiutare il token. Se l’app controlla soltanto l’esistenza del cookie, senza altro, mostrerà uno stato di autenticazione errato all’utente.

La correzione applicata al flusso non consiste nell’impostare un expires più lontano nel tempo — sarebbe un rimedio illusorio. Il flusso deve invece verificare la validità server-side, e reagire concretamente a un refresh fallito.

Nel login automatico, jwtLogin restituisce un booleano. Se l’endpoint rifiuta il refresh, l’hook elimina il JWT non più valido, e non apre la WebView autenticata:

const success = await jwtLogin(jwt);

if (success) {
  navigation.reset({
    index: 0,
    routes: [{ name: 'WebView' }],
  });
} else {
  await deleteJWT();
}

Questa decisione risolve due problemi contemporaneamente: evita di presentare una WebView apparentemente autenticata ma con una sessione server già scaduta, e impedisce di riprovare indefinitamente con lo stesso refresh token ormai non valido.

Il refresh riuscito deve inoltre aggiornare il cookie ricevuto dal server. Per questo persistCookiesFromResponse(res) viene eseguito anche in jwtLogin, non soltanto nel login iniziale come si potrebbe pensare a prima vista.

Un ulteriore dettaglio emerso durante il supporto multipiattaforma riguarda proprio il Keychain: iOS e Android possono usare API e identificatori differenti. Il servizio di autenticazione deve leggere e scrivere il refresh token nello stesso spazio logico, sempre. Se il token viene salvato con un service e letto senza quel service, l’app può comportarsi come se il token fosse scaduto o assente, anche quando la credenziale è in realtà ancora perfettamente memorizzata.

La diagnosi corretta di un errore “token scaduto” deve quindi controllare, nell’ordine: il codice HTTP e il payload restituito dall’endpoint di refresh; la claim exp del JWT, senza modificare il token; la presenza del refresh token nel Keychain corretto; la presenza e il dominio del cookie nella cookie store; la persistenza degli header Set-Cookie della risposta; l’orologio del dispositivo e del server; e infine il comportamento di fallback verso il login manuale.


Logout e pulizia dello stato

Il logout deve rimuovere entrambi i canali, sempre insieme. Cancellare soltanto il cookie può lasciare nel Keychain un refresh token ancora perfettamente utilizzabile; cancellare soltanto il JWT, invece, può lasciare la WebView apparentemente ancora autenticata.

Nel progetto sono presenti funzioni separate per i due livelli:

export async function clearAllCookies() {
  try {
    await CookieManager.clearAll();
  } catch (error) {
    console.error('Errore durante la pulizia dei cookie:', error);
  }
}

La pulizia completa deve essere coordinata con deleteJWT, deleteAuthToken, e con il logout del package di autenticazione — nessuno di questi passaggi va saltato. Dopo il logout è inoltre importante resettare la navigazione alla schermata di identificazione, così che l’utente non possa tornare indietro verso una WebView già caricata e apparentemente ancora attiva.


Errori comuni da evitare

Confondere cookie e JWT resta il primo e più diffuso errore. Sono due credenziali diverse tra loro: una soluzione che sincronizza soltanto il cookie non copre le chiamate native, mentre una soluzione che salva soltanto il JWT non copre la WebView.

Usare AsyncStorage per il token è un altro errore da evitare sempre. AsyncStorage è adatto a preferenze e flag non sensibili, punto. Il refresh token e le credenziali di autenticazione devono restare esclusivamente nel Keychain o nel Keystore, tramite un servizio dedicato.

Impostare una scadenza arbitraria troppo lunga può sembrare una scorciatoia comoda, ma non risolve nulla: un cookie ventennale non risolve la scadenza del JWT sottostante. Può, anzi, mantenere sul dispositivo uno stato obsoleto, e rendere più difficile la diagnosi quando qualcosa va storto.

Persistire più cookie in un’unica stringa è un altro errore tecnico da evitare: gli header Set-Cookie possono essere multipli, e contenere attributi con virgole al loro interno. È molto più sicuro gestire ogni valore separatamente, con setFromResponse.

Aprire la WebView prima della sincronizzazione è un errore di sequenza piuttosto insidioso: se il cookie viene impostato dopo la prima richiesta della pagina, il caricamento iniziale può avvenire senza sessione attiva. La scrittura deve sempre precedere l’apertura della route autenticata, mai seguirla.

Loggare token e cookie completi è un errore di sicurezza da evitare categoricamente. Durante il debug si possono loggare nome, dominio, scadenza e stato dell’operazione, ma mai il valore completo di JWT o cookie: un log finito in un sistema di raccolta errori diventa, di fatto, una copia della credenziale stessa.

Considerare thirdPartyCookiesEnabled una soluzione universale, infine, è un fraintendimento comune. Questa proprietà modifica soltanto una policy della WebView — non sincronizza Keychain e cookie store, non rinnova il JWT, e non corregge un dominio configurato in modo errato.


Domande frequenti

WebView e Axios condividono automaticamente i cookie?

No, e non è una garanzia su cui basare l’architettura del progetto. La condivisione dipende dalla piattaforma, dalla libreria e dalla configurazione della WebView. Nel codice nativo è sempre preferibile gestire esplicitamente la cookie store tramite CookieManager.

credentials: 'include' è obbligatorio?

È utile per indicare che la richiesta può includere credenziali, ma non sostituisce la sincronizzazione dei cookie. Il cookie deve prima essere presente nella store corretta, e il server deve accettare la richiesta secondo le proprie regole interne.

Posso prolungare un JWT modificando expires del cookie?

No, e questo è forse il fraintendimento più comune di tutti. expires riguarda esclusivamente il cookie. La validità del JWT è determinata dalla firma e dalle claim verificate dal server, in particolare da exp. Per ottenere una nuova sessione serve sempre un refresh token valido, oppure un nuovo login da capo.

Perché la WebView mostra ancora il profilo se il token è scaduto?

Perché il cookie può essere ancora presente localmente, mentre il backend ha già invalidato il token o la sessione lato server. Per questo il controllo di autenticazione deve sempre includere una verifica server-side, non fermarsi alla sola presenza locale del cookie.

Dove devo salvare il refresh token?

Nel Keychain su iOS, e nel Keystore su Android, attraverso una libreria o un package che uniformi il comportamento tra le due piattaforme. Non va mai salvato in AsyncStorage, né tantomeno nei log.

Quando devo usare setFromResponse?

Quando il cookie arriva dall’header Set-Cookie del server, e vuoi conservarne fedelmente tutti gli attributi. CookieManager.set è invece più utile quando devi creare manualmente un cookie a partire da nome, valore, dominio, path e scadenza definiti dall’app stessa.

Conclusione

Gestire i cookie tra WebView e API native non significa semplicemente copiare una stringa da un ambiente all’altro. Significa progettare una sessione composta da più elementi distinti, e stabilire con chiarezza quale componente sia responsabile di ciascuno: la WebView usa il cookie associato a my-genius.it; il codice nativo usa token recuperati dal Keychain; CookieManager sincronizza la cookie store; CookiesService verifica la sessione reale sul backend; il bridge trasferisce i token necessari senza mai esporre la persistenza nativa alla WebApp; e il flusso di auto-login elimina le credenziali non più valide, tornando al login manuale quando serve.

Il principio più importante, alla fine, è anche il più semplice da ricordare: la presenza di un cookie non dimostra affatto che la sessione sia ancora valida. Cookie expiration e JWT expiration restano due meccanismi diversi, governati da regole diverse. Solo distinguendoli con chiarezza è possibile evitare l’errore più comune nelle app ibride: una WebView che sembra autenticata, ma che in realtà non possiede più una sessione realmente utilizzabile.

Se stai integrando una WebApp in una shell React Native, considera cookie store, Keychain e bridge come parti di uno stesso disegno di autenticazione, non come tre problemi separati. Testare il login iniziale non basta mai: bisogna verificare anche la riapertura dell’app, il refresh, il logout, la scadenza, e la sincronizzazione su iOS e Android.


Fonti e riferimenti

Leggi prossimo articolo