firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

ID token refresh fails when using Persistence.Cookie auth persistence method

Open PhastPhood opened this issue 7 months ago • 4 comments

Operating System

MacOS Sequoia Version 15.1.1

Environment (if applicable)

All browsers

Firebase SDK Version

11.6.1

Firebase SDK Product(s)

Auth

Project Tooling

React app with Next.js

Detailed Problem Description

Saw that there was a new Persistence.Cookie strategy for Auth in beta and decided to give it a try, since it should fix some Auth persistence issues we have in Safari. I'm mostly following the implementation from this PR.

It works great, except if a user logs in and doesn't close their browser for an hour, and the ID token expires, Firebase fails to refresh it. This logs the user out. Other persistence strategies don't seem to have this issue. I'm seeing that the error code is user-token-expired.

This seems to be because:

  1. When PersistenceUserManager.getCurrentUser is called, the user is created using the stored cookie value. This user has refreshToken = null since the cookie value is just a string with the ID token and does not have the refresh token.
  2. When the ID token is about to expire or expired and getIdToken refreshed, this line fails since the refresh token is null.

Steps and code to reproduce issue

  1. Set auth persistence to browserCookiePersistence:
auth.setPersistence(browserCookiePersistence);
  1. Set up Next.JS middleware to handle browser cookie persistence (example)
  2. Without closing the browser tab/window, wait an hour so that the ID token expires (or lower the refresh interval so that the force refresh happens more quickly)

Expected result: User remains logged in, and ID token is refreshed. Firebase makes a request to /__cookies__ endpoint to refresh the ID token.

Actual behavior The ID token expires and is not refreshed. The error code received is user-token-expired. A DELETE request is sent to /__cookies__ endpoint to log the user out.

PhastPhood avatar May 13 '25 05:05 PhastPhood

I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.

google-oss-bot avatar May 13 '25 05:05 google-oss-bot

@PhastPhood thanks for giving this a spin. I'll be looking into this as I do my next push on Cookie Persistence.

I've found some concerning race conditions with setPersistence, curious if you'd have better experience if you passed browserCookiePersistence in as an option to initializeAuth.

If you're using popups and autoinit+emulators that does mean a bit more gymnastics:

import { initializeAuth, browserPopupRedirectResolver, connectAuthEmulator } from "firebase/auth";
import { getDefaults } from "@firebase/util";

const popupRedirectResolver = typeof window === 'undefined' ? undefined : browserPopupRedirectResolver;

export const auth = initializeAuth(app, {
    popupRedirectResolver,
    persistence: browserCookiePersistence,
});

const authEmulatorHost = getDefaults()?.emulatorHosts?.auth;
if (authEmulatorHost) {
    connectAuthEmulator(auth, `http://${authEmulatorHost}`);
}

jamesdaniels avatar May 14 '25 14:05 jamesdaniels

@jamesdaniels Thanks for taking a look and adding this feature, really appreciate it!

Some context: We currently are using indexedDBLocalPersistence. However, this has some issues on Safari – Safari wipes indexedDB after 7 days of not interacting with a site. The majority of our users are on mobile iOS and do not use our site weekly, so this means that they are constantly getting logged out, and it's one of our most frequent user complaints. We would love to start using cookie persistence, which does not have this issue.

Ideally, I would like to not log out any users who currently have their auth persisted in indexedDB, and we don't use popups, so my current approach while I've been testing has been:

Auth initialization

initializeAuth(getApp(), {
  persistence: [browserCookiePersistence, indexedDBLocalPersistence],
});

Before a new sign-in

auth.setPersistence(
  isSafari? browserCookiePersistence : indexedDBLocalPersistence
);

I have noticed two issues with the above approach so far:

  • If a user logs out from an indexedDB session, doesn't close the window, and now logs in using browserCookiePersistence while in the same session, it doesn't make the call to __cookies__ and doesn't persist the auth state in indexedDB or browser cookies
  • If a user logs in for the first time, it still calls the __cookies__ end point even if we setPersistence(indexedDBLocalPersistence) right before.

I'll give it a shot with just initializing with browserCookiePersistence.

Thanks again for the help! This feature would really alleviate some issues with our user experience!

PhastPhood avatar May 14 '25 15:05 PhastPhood

Ah gotcha, yeah I'll work in the upgrade path for browserCookiePersistence so that works as expected & continue to hunt for the setPersistence race condition that has so far eluded me.

jamesdaniels avatar May 15 '25 01:05 jamesdaniels

Small update on this – I didn't realize that auth.setPersistence was an async function which was causing some race conditions 😅. After fixing that, I think the main issue that I'm seeing is just the ID token not handling refreshing in the same way that indexedDB does

PhastPhood avatar Jun 20 '25 15:06 PhastPhood